Zum Inhalt springen
Micro Frontend

Ticketshop nativ einbetten

Das Micro-Frontend rendert den ADITUS Ticketshop direkt in deine Seite, ohne iFrame. Websites ohne Backend binden das Script ein und setzen public-key zusammen mit cart-scope="tab". Websites mit Backend erstellen die Session auf ihrem Server und übergeben den Session-Token; dein Server legt dann fest, wer der Käufer ist. Ohne Session bleibt das Micro-Frontend inaktiv.

Neu hier? Der Überblick zeigt ohne Technik, was das Micro-Frontend kann.

Schnellstart

Es gibt zwei Wege zum laufenden Shop. Beide nutzen dasselbe Script und dasselbe Element; sie unterscheiden sich darin, wer die Session erstellt.

Ohne Backend

Das Element erstellt selbst eine Browser-Session für deinen publicKey, beschränkt auf die freigegebenen Domains. Setze cart-scope="tab": ohne Server-Identität endet der Standard "shared" in einem expliziten Fehler. Jeder Browser-Tab hat dann seinen eigenen Warenkorb. Die Session ist anonym: Besucher stöbern und füllen den Warenkorb. Vor der Registrierung muss element.config.onUserRequired einen nutzergebundenen Session-Token von einem Server liefern; ohne ihn endet der Shop mit einem Hinweis im Warenkorb.

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>

Mit Backend

Dein Server erstellt die Session mit dem Mint-Secret und legt fest, wer der Käufer ist. Übergib den zurückgegebenen Token an das Element. Mit einer Käufer-E-Mail hält der Standard cart-scope "shared" einen gemeinsamen Warenkorb über alle 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>

Ersetze pk_your_site durch deinen Publishable Key aus dem ADITUS-Admin. developers.aditus.com ist die Produktionsadresse für Script und API. Beide Wege sind weiter unten ausführlich beschrieben.

Die Technologie hinter dem Micro-Frontend

Das Micro-Frontend rendert den kompletten Kaufprozess (Events, Artikel, Warenkorb, Registrierung, Zahlung, Abschluss) in das DOM deiner Seite. Es nutzt das Layout deiner Seite, und dein CSS kann jeden Teil davon gestalten.

React 19, kompatibel mit React 18

Der komplette Ablauf läuft als ein typsicherer React-Flow. Er wird mit React 19 gebaut und getestet und nutzt nur den React-18-Funktionsumfang (useState, useEffect, useMemo und Ähnliches), daher läuft das Quellpaket ab React 18 (peerDependency react >=18). Neben React hat es eine Runtime-Abhängigkeit: libphonenumber-js für die Prüfung von Telefonnummern. Es gibt keine UI-Bibliothek, kein Data-Fetching-Framework und kein Zustands-Toolkit. Das von ADITUS gehostete Script enthält React bereits, deine Seite braucht also kein eigenes React.

Light-DOM statt iFrame

Kein iFrame, kein Shadow-DOM. Der Shop rendert direkt in das DOM deiner Seite, fügt sich in dein Layout ein und ist responsiv und barrierefrei. Schriften und Farben kommen aus den --aditus-*-Tokens; die Standardschrift ist system-ui. Setze --aditus-font, um die Schrift deiner Seite zu verwenden.

Framework-agnostisch: ein mount(), überall

Ein einziger Aufruf (mount(container, config)) hängt den Shop in ein beliebiges DOM-Element. Ob React, Vue, Angular oder reines HTML: der Adapter ist framework-neutral und funktioniert überall gleich.

Das Styling gehört dir

Jedes Element trägt einen stabilen CSS-Hook (BEM-Klassen aditus-shop__* plus data-state). Du gestaltest es mit deinem eigenen CSS oder setzt Markenfarben, Radien und Abstände über ein paar --aditus-*-Design-Tokens. Die optionalen Basis-Styles liegen in CSS @layer aditus-shop, daher gewinnen deine Regeln außerhalb von Layern immer. Das gilt auch für allgemeine Regeln deiner Website wie button {}, input {} oder a {} aus einem WordPress- oder Bootstrap-Theme: Sie verändern den Shop ebenfalls. Beschränke solche Regeln auf deine eigenen Seitenbereiche, wenn der Shop sie nicht übernehmen soll.

Multilingual, live umschaltbar

DE/EN sind eingebaut. Ein Sprachwechsel wird über handle.update() direkt in die laufende Instanz gespielt, ohne die Journey des Käufers zurückzusetzen. Warenkorb und Fortschritt bleiben erhalten.

Sicher by Design

Ohne gültige Session bleibt der Shop inaktiv und stellt keine Shop-Anfragen. Die Session kommt entweder von deinem Server (erstellt mit dem Mint-Secret, das nie in den Browser gelangt) oder, bei Websites ohne Backend, aus der domain-beschränkten Browser-Session für deinen publicKey. Alle Shop-Aufrufe laufen über die ADITUS-API, die die Shop-Zugangsdaten serverseitig ergänzt.

Styling in drei Ebenen

Nutze die allgemeinste passende Ebene: Design-Tokens bilden die Markenbasis, allgemeine semantische BEM-Hooks gestalten wiederkehrende Rollen konsistent und bestehende komponentenspezifische Hooks bleiben Expert-Overrides vorbehalten.

Allgemeine semantische Hooks

.aditus-shop__section, .aditus-shop__panel, .aditus-shop__list, .aditus-shop__list-item, .aditus-shop__actions und .aditus-shop__action. Diese additiven Hooks beschreiben Rollen; jedes Element behält zugleich seinen spezialisierten Hook. Die URL- und Screenshot-Analyse im Styling-Studio schlägt Regeln genau auf dieser Ebene vor (Rahmen vs. Schatten, Button-Gewicht, Sektions-Abstand), nie auf komponentenspezifischen Hooks.

Aktionen und Ausnahmen

Wo implementiert, dokumentiert data-variant="primary" oder data-variant="secondary" die Rolle einer Aktion. Leite semantische Rollen nicht aus der DOM-Verschachtelung ab: reg-form ist kein dokumentiertes Panel. Nutze seinen spezialisierten Hook nur, wenn genau dieses Formular bewusst abweichen soll.

shop-micro-frontend.css
/* 1. Tokens: the broad brand foundation. */
.onsite-shop .aditus-shop {
  --aditus-accent: #c2410c;
  --aditus-surface: #ffffff;
  --aditus-radius: 12px;
}

/* 2. General hooks: consistent roles throughout the journey. */
.onsite-shop .aditus-shop__section { margin-block: 2rem; }
.onsite-shop .aditus-shop__panel {
  background: var(--aditus-surface);
  border: 1px solid var(--aditus-border);
  border-radius: var(--aditus-radius);
  box-shadow: var(--aditus-shadow);
}
.onsite-shop .aditus-shop__list { gap: 0.75rem; }
.onsite-shop .aditus-shop__list-item { padding-block: 0.75rem; }
.onsite-shop .aditus-shop__actions { gap: 0.5rem; }
.onsite-shop .aditus-shop__action[data-variant="primary"] { font-weight: 700; }
.onsite-shop .aditus-shop__action[data-variant="secondary"] { box-shadow: none; }

/* 3. Expert override: only the named component changes. */
.onsite-shop .aditus-shop__article-card {
  border-inline-start: 4px solid var(--aditus-accent);
}

Voraussetzungen an deine Website

Die Voraussetzungen hängen vom Auslieferungsweg ab. Das von ADITUS gehostete Script braucht nur einen aktuellen Browser; das Quellpaket braucht zusätzlich React 18+ in deiner App. Beide brauchen eine Session für einen publicKey, auf dessen Whitelist deine Domain steht.

Was gebraucht wird

  • Nur beim Quellpaket: React 18+ und react-dom 18+ im Bundle deiner App (peerDependency react >=18); mount() nutzt createRoot aus react-dom/client. Das gehostete Script bringt sein eigenes React mit.
  • Ein Browser mit DOM (client-seitig). Der Adapter mountet in ein echtes DOM-Element (Light-DOM). SSR-Apps sind ok, solange mount() clientseitig läuft.
  • Ein aktueller Evergreen-Browser ist erforderlich. Der Adapter nutzt Standard-Web-APIs (fetch, AbortController, ResizeObserver, CSS @layer), seit ~2022 Baseline. Kein IE.
  • Eine Session für deinen publicKey. Websites ohne Backend setzen public-key zusammen mit cart-scope="tab"; das Element erstellt seine Browser-Session dann selbst über den domain-beschränkten /shop/embed-session. Websites mit Backend erstellen die Session auf ihrem Server mit dem Mint-Secret und übergeben den Token. Ohne Session bleibt das Micro-Frontend inaktiv. Deine Domain muss auf der Whitelist des publicKey stehen.

Was nicht nötig ist

  • Kein bestimmtes Framework. mount(container, config) funktioniert mit React, Vue, Angular oder reinem HTML.
  • Kein iFrame, kein Shadow-DOM. Es rendert nativ in deine Seite.
  • Kein bestimmtes CSS auf deiner Seite. Die Basis-Styles liegen in @layer aditus-shop auf Single-Class-Spezifität, daher gewinnt jede ungelayerte Regel deiner Seite. Das gilt auch für allgemeine Element-Regeln wie button, input oder a: Sie treffen auch die Bedienelemente des Shops. Begrenze sie auf deine eigenen Inhalte, wenn sie dort nicht greifen sollen. Box-Sizing, Innenabstände und Abstände sind zusätzlich gegen einen globalen Reset wie * { margin:0; padding:0 } geschützt. baseStyles: false schaltet die Basis-Styles ab.
  • Keine weiteren Bibliotheken auf deiner Seite. Das Quellpaket bringt neben den React-peerDependencies eine Runtime-Abhängigkeit mit (libphonenumber-js für die Prüfung von Telefonnummern); das gehostete Script enthält alles.
  • Kein Build-Setup beim gehosteten Script; das Quellpaket braucht nur einen ESM-Import.

Deine Website ist vertrauenswürdig: Dein Server authentifiziert den Nutzer, bevor er eine identifizierte Session erstellt. Das Embed läuft bewusst im Light DOM deiner Seite, ohne iFrame-Isolation. Scripts deiner Seite können auf DOM und Browser-Speicher zugreifen; sichere deshalb deine Seite und ihre Drittanbieter-Scripts ab. Origin-Autorisierung per Public Key ist keine Nutzerauthentifizierung. Zahlungszugangsdaten werden nur auf der externen Seite des Zahlungsanbieters eingegeben.

Zahlungssicherheit, PCI & CSP

Die Kaufvorbereitung rendert in deiner Seite. Die Zahlung selbst findet auf der Seite des Zahlungsanbieters statt, daher laufen keine Zahlungsdaten durch deine Seite.

Keine Zahlungsdaten in deiner Seite

Die komplette Kaufvorbereitung (Event, Tickets, Add-ons, Warenkorb, Registrierung) rendert nativ in deiner Seite. In dem Moment, in dem der Benutzer eine Redirect-Zahlart startet, verlässt der sensible Datenstrom deine Seite vollständig: Eine normale Top-Level-Navigation übergibt an die dedizierte, externe Payment-Page des Zahlungsanbieters, der Rückweg setzt die Journey fort und schließt die Bestellung ab. Karten- oder Bankdaten werden ausschließlich dort eingegeben. Das Micro-Frontend rendert, transportiert und speichert in deiner Seite keinerlei Zahlungsdaten.

Minimaler PCI-Scope

Da zu keinem Zeitpunkt Kreditkarten- oder Bankdaten durch deine Website verarbeitet, transportiert oder gespeichert werden, bleibt deine Umgebung im minimalen PCI-DSS-Scope des klassischen Redirect-Modells (typischerweise SAQ A; die verbindliche Einstufung nimmt immer dein Acquirer bzw. QSA vor). Zahlarten ohne Redirect, etwa Rechnung, schließen server-seitig ab, ganz ohne Zahlungsdaten im Browser.

Content Security Policy

Wenn deine Website eine Content Security Policy setzt, ergänze: script-src für die ADITUS-Origin (gehostetes Script), connect-src für die ADITUS-API-Origin (jeder Shop-Aufruf läuft darüber), img-src für die ADITUS-API-Origin (Flaggen, Zahlart-Logos, Wallet-Badges) und die Hosts deiner Event- und Artikelbilder sowie style-src 'unsafe-inline', weil die Basis-Styles als <style>-Element eingefügt werden (nicht nötig mit baseStyles: false und eigenem Stylesheet). Einträge in script-src oder frame-src für Zahlungsanbieter sind nicht nötig, weil der Zahlungsschritt ein Top-Level-Redirect ist.

Vergleich mit iFrame und Shadow-DOM

iFrame

Ein iFrame ist ein eigenes Dokument: Die Styles deiner Seite greifen darin nicht, seine Höhe muss synchronisiert werden, und es hat eine eigene Fokusreihenfolge. Das Micro-Frontend rendert die Kaufvorbereitung stattdessen als HTML im DOM deiner Seite. Nur die Zahlung verlässt deine Seite, per Top-Level-Redirect auf die Seite des Zahlungsanbieters.

Shadow-DOM

Viele Web Components kapseln sich per Shadow-DOM. Die Styles deiner Seite erreichen sie dann nur über Custom Properties und ::part. Das Micro-Frontend nutzt stattdessen die normale CSS-Kaskade: Die Basis-Styles liegen in @layer aditus-shop, daher gewinnt jede ungelayerte Regel deiner Seite ohne höhere Spezifität oder !important.

Die Ansätze im Vergleich

KriteriumKlassischer iFrameShadow-DOM-WidgetADITUS Micro-Frontend
BarrierefreiheitEigenes Dokument mit eigener Fokusreihenfolge und Seitenstruktur.ID-basierte ARIA-Verweise (aria-labelledby, aria-describedby) wirken nicht über die Shadow-Grenze hinweg.HTML im DOM deiner Seite: eine Fokusreihenfolge und eine Seitenstruktur mit deinem eigenen Markup.
Styling & BrandingStarr: keine CSS-Vererbung, Anpassung nur über postMessage-APIs.Globale Styles greifen nicht; Theming nur über durchgereichte Properties und Parts.Dein ungelayertes CSS gewinnt gegen die Basis-Styles in @layer; die Basis-Styles sind überschreibbar oder abschaltbar.
Bundle & PerformanceLädt typischerweise eine komplette zweite App samt Runtime im Frame.Bringt häufig eine eigene Runtime und doppelte Abhängigkeiten mit.Gehostetes Script: ein Bundle samt React. Quellpaket: nutzt das React deiner App plus eine Runtime-Abhängigkeit (libphonenumber-js).
Zahlungssicherheit & PCI-ScopeStark isoliert, aber um den Preis von UX, Design und Responsivität.Hängt vom Widget ab: Kartenfelder im Widget sind Teil deiner Seite.Auswahl in deiner Seite, Zahlung auf der Seite des Zahlungsanbieters.

Unsere Empfehlung: JavaScript direkt über die ADITUS-URL

Binde das JavaScript-Bundle direkt über die ADITUS-URL ein. Das ist unser empfohlener Standard für alle Websites. Er gilt für React und andere Frameworks ebenso wie für HTML- und CMS-Seiten. ADITUS verbessert die interne Shop-Logik zentral. Kompatible Verbesserungen erreichen deine Integration, ohne dass du ein neues Paket herunterladen, deine Website neu bauen oder veröffentlichen musst.

Zur empfohlenen Script-Einbindung /api/embed/v1/aditus-shop.js

So kommen Updates bei dir an

Die v1-URL liefert kompatible Freigaben mit stabilen Schnittstellen. Neue Freigaben werden bei späteren Seitenaufrufen geladen, sobald der derzeit einstündige HTTP-Cache abläuft oder neu validiert wird. Das geschieht nicht zwingend bei jedem Neuladen. Ein bereits geöffneter Checkout behält seinen geladenen Code.

Alternative: Auslieferung und Updates selbst verwalten

Wähle das Quellpaket, die Installation des Tarballs per npm oder eine selbst ausgelieferte JavaScript-Kopie nur, wenn du die Auslieferung bewusst selbst verwalten möchtest. ADITUS kann deine Kopie nicht aktualisieren. Neue Versionen prüfen, herunterladen oder installieren, testen, bei Bedarf neu bauen und veröffentlichen liegt dann bei dir. Das Paket ist nicht in der öffentlichen npm-Registry veröffentlicht.

Alternative: Quellpaket (selbst ausgeliefert)

Alternativ lädst du das Quellpaket von deiner ADITUS-Instanz herunter und bündelst es in deine App (Vite, webpack, Next.js oder Nuxt). Diese Auslieferung verwaltest du selbst: Updates erfordern Neuinstallation, Build und Deployment. Es liegt nicht auf öffentlichem npm.

Was die Lieferung enthält

Der komplette Adapter als lesbarer TypeScript-Quellcode mit Typdeklarationen für jede Config-Option, jeden Callback und jedes Event. React und react-dom bleiben peerDependencies. Deine Seite stellt sie bereit, nichts kollidiert im Bundle, es gibt kein zweites React.

So kommst du an das Paket

Das Paket ist für alle Websites identisch und lässt sich direkt von dieser Instanz herunterladen, ganz ohne Zugangsdaten: aditus-shop-embed.tgz. Installiere direkt von dieser URL (npm install akzeptiert Tarball-URLs, Pfad /api/embed/v1/aditus-shop-embed.tgz) oder entpacke ins Repo. Das Onboarding liefert deinen publicKey und die Domain-Whitelist; ohne publicKey bleibt das Embed inaktiv. Kontakt: Kontaktseite. Nutze dieses Paket nur, wenn du deinen eigenen Build auslieferst und aktualisierst.

Bleib aktuell: Der v1-Track bekommt kompatible Updates an Ort und Stelle. Prüfe regelmäßig manifest.json (vor jedem Release deiner App, am besten als geplanter Job) und vergleiche package.sourceHash mit aditus.sourceHash in der installierten package.json. Weichen sie ab, installiere den Tarball neu und lass Build und Tests laufen; Hooks, Tokens und Events bleiben innerhalb von v1 stabil. Das Web-Component-Bundle aktualisiert sich über den einstündigen HTTP-Cache von selbst.

Script-Einbindung über die ADITUS-URL

Das vorgebaute Bundle unter https://developers.aditus.com/api/embed/v1/aditus-shop.js funktioniert auf allen Websites, auch in Apps mit Build-Schritt, WordPress, Typo3 und statischem HTML. Ein Script-Tag registriert <aditus-shop> samt React: kein npm und kein eigener Build nötig. Die öffentliche, versionierte URL braucht keine Zugangsdaten; der Shop erfordert weiterhin einen freigeschalteten publicKey und eine Session. Ein laufender Checkout wird nie mitten im Ablauf aktualisiert; der Browser-Cache kann ein neues Bundle um bis zu eine Stunde verzögern.

index.html
<!-- 1. Das von ADITUS gehostete Bundle einbinden (einmal pro Seite). -->
<script src="https://developers.aditus.com/api/embed/v1/aditus-shop.js" defer></script>

<!-- 2. Explizite Tab-Isolation für automatisch im Browser erstellte anonyme Sessions (nicht der Standard).
     Für shared: session-token aus deinem Backend, anonym zusätzlich serverseitig sharedCartKey. -->
<aditus-shop
  public-key="pk_live_deinkey"
  api-base="https://developers.aditus.com/api"
  cart-scope="tab"
  culture="de-DE"
></aditus-shop>

<!-- 3. Optional: Journey-Events als ganz normale DOM-Events konsumieren. -->
<script>
  document.querySelector("aditus-shop").addEventListener("cart:update", (e) => {
    console.log("Positionen im Warenkorb:", e.detail.itemCount);
  });
</script>

Dieselbe gehostete JS-URL registriert automatisch <aditus-shop> und stellt window.AditusShop bereit: mount, themeToCss, clearJourney, mintEmbedSession und onEmbedSessionRenewed mit den Signaturen des Quellpakets. Deine Website benötigt kein React. Hosted mount, element.config und Quellpaket akzeptieren dieselbe AditusShopConfig inklusive Themes, eventTheme: false, articleContent und Callbacks. Nur reine HTML-Attribute können keine Funktionen oder Objekte übertragen. Das Quellpaket bleibt für Bundler und die gemeinsame Nutzung deiner vorhandenen React-Runtime gültig.

Warenkorb-Scope, Vertrauen und Speicherung

Plain JS: expliziter Token-Lebenszyklus

mount(container, config) ist derselbe Source-Mount: Ohne sessionToken bleibt er inaktiv und erstellt keine eigene Session. Setze apiBase explizit auf die vollständige /api/shop-URL. Nach dem asynchronen Login auf deiner Website erstellt dein Backend die Session server-zu-server und übergibt nur den Token an diese Browser-Funktion. host.renewSession und host.revokeSession sind deine eigenen Funktionen/Routen, die du im authentisierten Backend implementieren musst, keine funktionierenden ADITUS-Browser-Endpunkte. Secrets gelangen nie in den Browser. onUserRequired bleibt ein asynchroner Login mit serverseitiger Session-Erstellung, der Token oder null liefert, in jeder JavaScript-Variante.

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 und Renewal

Bei freigeschaltetem publicKey liefert mintEmbedSession(publicKey, apiRoot = "/api", { fresh?, staleToken? }) ohne Secret Promise<string | null>. Helfer und HTML-Attribut api-base verwenden die API-Wurzel mit /api; config.apiBase die Shop-Basis mit /api/shop. onEmbedSessionRenewed(publicKey, apiRoot, listener) übergibt einen Token-String und liefert eine Abmeldefunktion. Das Beispiel behandelt Fehler bei der Session-Erstellung, Refresh nach Ablauf, proaktive Erneuerung und Teardown. Abmelden entfernt nur diesen Listener, nicht gemeinsame Session-Caches, Renewal-Timer oder Server-Sessions; Browser-Helfer überbrücken Reloads bewusst per 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>

Attribute und vollständige JavaScript-Config

element.config akzeptiert die vollständige AditusShopConfig inklusive Funktionen. Definierte Config-Schlüssel überschreiben passende HTML-Attribute, auch false; fehlende/undefined-Schlüssel lassen Attribute gelten. Zuweisungen ersetzen alle Property-Overrides, keine Patches: element.config = {} entfernt sie. Zuweisung vor dem Custom-Element-Upgrade wird unterstützt. api-base ist eine API-Wurzel (/api), config.apiBase endet dagegen mit /api/shop. themeToCss(theme, selector = ".aditus-shop") liefert Token-CSS alternativ zum Theme-Objekt.

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>
AttributBedeutung
cart-scopeAditusShopConfig.cartScope?: 'tab' | 'shared' hat den Standard 'shared', auch beim Weglassen. 'tab' ist eine explizite Wahl für isolierte Journeys. Das zentral gehostete window.AditusShop.mount(), element.config des Custom Elements und mount() im Framework-/Quellpaket akzeptieren dieselbe Option; reines HTML nutzt cart-scope="tab" oder cart-scope="shared". Entscheide vor dem Mount. Der Scope ist für die gemountete Checkout-Instanz unveränderlich: handle.update() lehnt eine Änderung ab, statt einen laufenden Warenkorb zu migrieren. Behalte ihn in vollständigen Ersatz-Configs bei.
public-keyDein Publishable Key (Domain-Whitelist greift). Ohne session-token erstellt das Element seine Browser-Session selbst. Websites ohne Backend nutzen diesen Modus und setzen zusätzlich cart-scope="tab"; mit dem Standard "shared" endet die Browser-Session in einem expliziten Fehler, weil keine vertrauenswürdige Server-Identität vorliegt.
session-tokenEin Session-Token, den dein Backend mit dem Mint-Secret erstellt hat (siehe unten). Ist er gesetzt, erstellt das Element nie selbst eine Session; deine Seite verwaltet den Session-Lebenszyklus.
api-baseBasis-URL der ADITUS-API. Default ist die Origin, von der das Bundle-Script geladen wurde. Normalerweise setzt du das nie.
cultureShop-Culture/-Locale für die Auswahl der ADITUS-Ressourcen; gültige Werte wie it-IT werden weitergegeben. Gutschein- und Upsell-Ressourcentexte bleiben wie von ADITUS geliefert; die übrigen Oberflächentexte des Embeds nutzen deutsche/englische Ressourcen und Fallback. Das bedeutet keine vollständig italienische UI. Eine Änderung aktualisiert die Sprache an Ort und Stelle, ohne die Journey zurückzusetzen.
article-layout / article-group-layout / event-layout / article-select-mode / show-headerDie flachen Darstellungsoptionen, gleiche Werte wie in der mount()-Config (cards, grid, quantity, …). Der Einstiegspunkt (Event-Auswahl vs. gepinntes Event) ist kein Attribut: er wird serverseitig beim Erstellen der Session festgelegt.
include-shop-assortmentsOptionale Sortimentsmischung; includeShopAssortments in mount(). Standard false: nur das Standardsortiment laden. Mit true zusätzlich weitere Sortimente mit showInShop=true laden. Ein ausdrücklich gewähltes Startsortiment hat immer Vorrang. Ohne Attribut gilt der hinterlegte Client-Default; explizites false überschreibt ihn.
survey-columnsSpaltenzahl (1–4) der Antwortoptionen von RadioButtonList-/CheckBoxList-Surveyfragen. Dies entspricht surveyColumns in der mount()-Config. Ohne Attribut bleibt es einspaltig (bzw. beim serverseitig hinterlegten Client-Default); schmale Viewports fallen auf eine Spalte zurück.
hide-house-numberBlendet das Hausnummernfeld im Adressblock der Surveys in jeder Ländervariante aus; dies entspricht hideHouseNumber in der mount()-Config. Straße und Hausnummer werden dann gemeinsam in einem Feld „Straße und Hausnummer“ erfasst, das die frei gewordene Breite übernimmt; Adressvorschläge tragen Straße und Hausnummer in der landesüblichen Reihenfolge dort ein (z. B. "12 Rue Cler" in Frankreich), eine vorbefüllte Hausnummer wird geleert, und die separate Hausnummer wird immer als unsichtbares Leerzeichen ohne Breite (Zero-Width-Space, U+200B) übermittelt, ohne Validierungsfehler, auch wenn der Survey sie als Pflichtfeld markiert. Das Rechnungsadressformular folgt derselben Regel. Standard aus; ohne Attribut gilt der hinterlegte Client-Default, explizites "false" überschreibt ihn.
show-jump-buttonSprung-Button im Personalisierungsschritt; entspricht showJumpButton in der mount()-Config. Standardmäßig an: Nach einem fehlgeschlagenen Absenden klebt ein kleiner Button unten am Shop, zeigt die Zahl der offenen Angaben und springt zur nächsten; ist alles ausgefüllt, führt er zurück zum Button, ohne ihn je auszulösen. Er ist ausgeblendet, solange dieser Button oder das Ziel sichtbar ist und während getippt wird. Mit "false" ausschalten; ohne Attribut gilt der hinterlegte Client-Default.
voucher-code-paramName eines Query-Parameters in der URL deiner Seite, der einen Gutschein-/Ticketcode trägt (z. B. "code" für Links wie …/tickets?code=ED24). Dies entspricht voucherCodeParam in der mount()-Config. Das Element liest ihn einmal beim Mount, entfernt ihn aus der Adresszeile und löst den Code automatisch ein (Gutschein-Maske oder erster Warenkorb). Nur bei Sessions mit festgelegtem Event.
base-stylesAuf "false" gesetzt entfällt der neutrale Default-Skin und du stylst jeden Hook selbst.
autoScroll / articleIconVisible / classPrefixKein Attribut: diese Optionen setzt du über element.config (gleiche Namen wie in der mount()-Config). autoScroll (Standard true) scrollt den Shop-Anfang nach einem Schrittwechsel wieder ins Bild; setze false, wenn deine Seite das Scrollen selbst steuert. articleIconVisible (Standard false) zeigt in der Listen-Darstellung je Artikel ein Bild oder Monogramm. classPrefix (Standard "aditus-shop") ändert das Präfix aller CSS-Hooks.

Session & Selbstheilung

Ein effektiver sessionToken (aus Config oder Attributen) deaktiviert die Session-Erstellung im Browser. Mit effektivem publicKey ohne Token erstellt das Element weiterhin seine Browser-Session. Ein eigenes refreshSession deiner Seite hat immer Vorrang vor eingebautem Refresh. Ohne ihn nutzen selbst erstellte Sessions den gemeinsamen Browser-Helfer für Refresh und proaktive Erneuerung. Light DOM bleibt unverändert: kein iFrame oder Shadow DOM.

Events & Versionierung

DOM-CustomEvents propagieren (bubbles: true) vom Element mit Payload in event.detail: cart:update, phase:change, checkout:complete und analytics, mit denselben Namen/Payloads wie handle.on(). Config-Callbacks sind ebenfalls verfügbar.

Journey gezielt zurücksetzen

element.clearJourney() und window.AditusShop.clearJourney() entfernen nur den lokalen Wiederaufnahme-Zeiger dieser Seite. Vor erneutem Einfügen oder Mount aufrufen, um frisch zu starten. Sie löschen keine Server-Warenkörbe und widerrufen keine Sessions; Logout-Widerruf gehört ins Backend.

Updates ohne Remount

mount/update-Configs sind vollständige Ersetzungen, keine Patches: Bewahre deine vollständige Config und übergib { ...config, culture } an handle.update(). element.config ersetzt ebenfalls alle Property-Overrides; nicht überdeckte Attribute aktualisieren weiterhin in place. Darstellung erhält die Journey, Session-Identitätswechsel etabliert die Session neu. handle() unmountet; Entfernen des Elements räumt Embed- und Renewal-Listener auf. Eigene DOM-Listener an einem behaltenen Element musst du selbst entfernen.

SSR-Apps: Next.js & Nuxt

Das Micro-Frontend ist bewusst client-only. Sein Inhalt ist live und session-gebunden: Verfügbarkeiten, Preise und der Warenkorb des Käufers existieren nur für eine authentifizierte Session zum Abrufzeitpunkt. Es gibt also nichts Sinnvolles, das dein Server vorrendern könnte. mount() erzeugt einen frischen client-seitigen React-Root (createRoot); server-gerendertes Markup im Container wird nicht hydriert. Genau das macht die SSR-Integration einfach und vorhersehbar: auf dem Server einen Platzhalter rendern, im Client mounten, den Platz reservieren. So springt nichts.

ticketshop-section.tsx
// Next.js (App Router): Das Micro-Frontend ist bewusst client-only.
"use client";
import { useEffect, useRef } from "react";
import { mount, type ShopHandle } from "@aditus/shop-embed";

export function TicketshopSection({ sessionToken }: { sessionToken: string }) {
  const el = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!el.current) return;
    const handle: ShopHandle = mount(el.current, {
      publicKey: "pk_deine_seite",
      sessionToken, // server-seitig ausgestellt, als Prop an die Client-Komponente
      cartScope: "shared", // Standard auch im Framework; vertrauenswürdige Server-Identität
    });
    return () => handle(); // Unmount beim Routenwechsel
  }, [sessionToken]);

  // Server und Client rendern DENSELBEN Platzhalter und vermeiden damit einen Hydration-Mismatch.
  // min-height reserviert den Platz, damit die Seite beim Mount nicht springt (CLS).
  return <div id="aditus-shop" ref={el} style={{ minHeight: 560 }} />;
}

Layout-Sprünge vermeiden (CLS)

Gib dem Mount-Container eine min-height, die grob der ersten Shop-Ansicht entspricht, und rendere darin optional ein eigenes Skeleton. Server und Client erzeugen denselben Platzhalter, es gibt keinen Hydration-Mismatch. Der Adapter rendert direkt nach dem Mount und ersetzt den Platzhalter in einem Paint. Versuche kein hydrateRoot auf dem Container: Es gibt keinen server-gerenderten Shop-Baum, an den sich React heften könnte.

Die Session in einer SSR-App

SSR-Apps haben einen natürlichen Vorteil: Dieselbe Server-Runtime, die die Seite rendert, kann die Session auch server-zu-server mit dem Secret erstellen, genau wie im Session-Ablauf weiter unten (Next.js: Route Handler oder Server Component; Nuxt: Server-Route). Reiche den entstandenen opaken Token als Prop oder Payload an die Client-Komponente. Das Secret taucht nie im Client-Code auf, der Token bleibt im Speicher.

Next.js im Detail

Markiere die mountende Komponente mit "use client" und mounte im useEffect (siehe Snippet). Im Pages Router erreicht next/dynamic mit ssr: false dasselbe. Gib das Handle als Cleanup aus dem Effect zurück, damit client-seitige Routenwechsel sauber unmounten. Ein Sprachwechsel an der laufenden Instanz läuft über handle.update(), ohne die Journey zurückzusetzen; ein neuer Session-Token mountet neu (wie im Snippet). Auch das übersteht der Warenkorb, weil das Micro-Frontend die Journey beim Mount wiederherstellt.

Nuxt / Vue im Detail

Kapsle das Mount-Ziel in <ClientOnly> oder mounte in onMounted. Der Adapter braucht ein echtes DOM-Element im Browser. Denk an die Bundle-Voraussetzung: react und react-dom 18+ müssen als Dependencies deiner Nuxt-App installiert sein; der Shop rendert in seinen eigenen Container und kollidiert nicht mit Vues Virtual DOM.

Warenkorb-Scope, Vertrauen und Speicherung

cartScope legt fest, ob die Tabs eines Besuchers einen gemeinsamen Warenkorb nutzen. Standard ist 'shared'; 'tab' trennt die Tabs bewusst. Lege den Scope vor dem Mount fest, danach lässt er sich nicht mehr ändern.

Aspekt'shared' (Standard)'tab'
WarenkorbEin Warenkorb für alle Tabs mit derselben Origin, demselben publicKey, derselben Identität und VeranstaltungJeder Tab hat seinen eigenen Warenkorb
SessionVon deinem Backend erstellt: identifizierter Nutzer oder anonym mit sharedCartKey aus deiner eigenen Server-Session. Identität und Schlüssel nie aus dem Browser übernehmen.Jede Session, auch eine im Browser erstellte
Mehrere TabsEin Tab bearbeitet, die anderen sind schreibgeschützt und bieten „Hier weiterbearbeiten“. Ein Tab, der neu geöffnet oder neu geladen wird, übernimmt automatisch, während der Zahlung nie.Unabhängig
Fehlt die VoraussetzungExpliziter Fehler, auch ohne angegebenen cartScope; kein stiller Wechsel zu 'tab'. Seiten ohne Backend setzen deshalb 'tab'.–
  • Konfiguration: cartScope in mount() und element.config, cart-scope im HTML. handle.update() lehnt eine Änderung ab; übernimm den Wert deshalb in jede Ersatz-Config. Attribute · Session-Felder
  • Speicherung: Formularentwürfe liegen pro Tab in sessionStorage und verfallen 2 Stunden nach der letzten Bearbeitung. Tokens landen nie in localStorage. Bestätigte Bestellungen und Registrierungen sind davon nicht betroffen.
  • Logout: Bei Logout oder Nutzerwechsel widerruft dein Backend die alte Session; danach aktualisierst du den Token oder unmountest das Embed und rufst clearJourney() auf. Ein neuer Nutzer übernimmt nie den Warenkorb des vorherigen.
  • Sicherheit: Das Embed rendert ohne iFrame direkt in deine Seite. Scripts deiner Seite, auch von Drittanbietern, können darauf zugreifen; binde deshalb nur vertrauenswürdige ein. Zahlungsdaten werden ausschließlich beim Zahlungsanbieter eingegeben.
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();

Das Micro-Frontend braucht eine Session

Ohne Token

Ohne Session-Token: das Micro-Frontend bleibt inaktiv

Ohne Session-Token rendert das Micro-Frontend nur einen neutralen Hinweis und startet keine API-Calls. Es kann kein Warenkorb gefüllt werden, keine Registrierung, kein Checkout. Eine Session ist Pflicht, bevor der Shop aktiv wird. Sie muss aber nicht an einen bekannten Nutzer gebunden sein: Eine Gast-Session genügt zum Browsen und Füllen des Warenkorbs, der echte Nutzer kann später beim Checkout festgelegt werden (siehe den Optional-User-Flow unten).

mount.ts
import { mount } from "@aditus/shop-embed";

const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount-Ziel #aditus-shop nicht gefunden");

// Ohne sessionToken bleibt das Micro-Frontend inaktiv: es zeigt nur einen
// neutralen Hinweis und startet keine API-Calls: kein Warenkorb,
// keine Registrierung, kein Checkout. Erst eine Session aktiviert den Shop.
mount(el, {
  publicKey: "pk_deine_seite",
  // sessionToken fehlt -> Shop wird nicht aktiviert.
});
Nutzer deiner Website

Session-Token für einen konkreten User

Dein Backend stellt einen kurzlebigen Token aus, der an genau einen Shop-User gebunden ist. Das Micro-Frontend agiert dann als dieser User: dessen Warenkorb, dessen Daten. Der Token ist opak, ablaufend, single-user und an deinen publicKey gebunden, und dein Secret erreicht nie den Browser.

mount.ts
import {
  mount,
  type ShopHandle,
  type AditusShopConfig,
} from "@aditus/shop-embed";

// getElementById kann null sein. Prüfe dies in TypeScript sauber statt mit "!".
const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount-Ziel #aditus-shop nicht gefunden");

// AditusShopConfig typisiert alle Optionen und bietet Autovervollständigung.
const config: AditusShopConfig = {
  apiBase: "https://developers.aditus.com/api/shop",
  publicKey: "pk_deine_seite", // Pflicht, sobald eine Session im Spiel ist
  sessionToken,                // vom Backend ausgestellt, nur im Speicher halten
  cartScope: "shared",          // Standard; vertrauenswürdige Server-Identität erforderlich
};

const handle: ShopHandle = mount(el, config);

// Das Micro-Frontend sendet bei jedem Shop-Call den Header X-Aditus-Session.
// Beim Logout die Session serverseitig widerrufen und neu mounten/aktualisieren.

Beim Reload kann das Embed mit einem gültigen Journey-Verweis den Live-Warenkorb unter demselben autorisierten Scope und derselben vertrauenswürdigen Identität vom Server laden. Der Verweis ist keine Autorisierung und garantiert keine Wiederherstellung nach Session-/Warenkorbablauf, Speicherlöschung oder Nutzerwechsel. Explizites tab führt Journeys anderer Tabs nicht zusammen; der Standard shared erzwingt weiterhin die Checkout-Sperre mit nur einem Schreiber.

Die Zahlungsrückkehr nutzt aditus-return (einen opaken, kurzlebigen, servergebundenen Rückkehr-Token) plus aditus-pay. Sie kann im selben oder in einem neuen Tab erfolgen; der Server verlangt aber einen autorisierten Client/eine autorisierte Origin und die passende vertrauenswürdige Identität. Rückkehr-Queryparameter sind kein Kaufnachweis. Der Server klärt den Zahlungsstatus und finalisiert den tokengebundenen Versuch; ein unbekannter Ausgang bleibt zur Klärung gesperrt, statt blind als neuer Kauf wiederholt zu werden. Abgelaufene oder unautorisierte Rückkehrversuche liefern explizite Fehler, keine Erlaubnis für eine Ersatzbestellung.

Die Zuweisung eines Zahlungsanbieters bereitet die Zahlung nur vor: Die Zusammenfassung bleibt bearbeitbar. Die Sperre beginnt erst beim tatsächlichen Absprung nach den erforderlichen Zustimmungen. Ein cancel-/error-Rückkehrparameter beweist keinen Fehlschlag: Das Backend prüft den Warenkorb erneut; Completed mit order.id bestätigt den Abschluss. Erst die bestätigt erfolgreiche Entfernung der Anbieterzuweisung für denselben Warenkorb erlaubt einen erneuten Versuch; unklare Ausgänge bleiben gesperrt und können erneut geprüft werden. Das garantiert weder eine Erstattung noch den Ausschluss einer späteren Zahlungsabwicklung.

Absprung zur Zahlungsseite: was deine Seite für die Session tun muss

  • Liefere die konfigurierte Rückkehr-URL mit gemountetem Embed aus. Erhalte aditus-return und aditus-pay beim Routing oder bei nötigen Login-Redirects; behandle aditus-pay=success niemals als Autorisierung. Stelle eine gültige Session für dieselbe vertrauenswürdige Identität und denselben publicKey bereit. Behalte den gewählten cartScope bei und lösche weder die Journey noch starte einen zweiten Checkout, um eine laufende, abgelaufene oder unklare Rückkehr zu umgehen.

Du willst stattdessen bewusst von vorn beginnen, etwa mit einem echten "Neu starten"-Button? Das Paket exportiert clearJourney(): Rufe es vor dem Mounten (oder Re-Mounten) auf, und der lokal gespeicherte Journey-Datensatz der aktuellen Seite wird gelöscht. Der nächste Mount startet frisch am Einstiegspunkt. Es wird nur der lokale Wiederaufnahme-Zeiger entfernt; serverseitiger Warenkorb-Zustand bleibt unberührt.

Einstiegspunkt: Veranstaltungsstartseite, Artikel oder Gutschein

Wo die Journey beginnt, entscheidet dein Server beim Erstellen der Session, nie der Browser. Ohne eventSlug öffnet das Micro-Frontend die Veranstaltungsübersicht. Mit eventSlug wählst du per startMode Artikelauswahl (Standard), Veranstaltungsstartseite oder Gutscheinmaske; assortment beschränkt die Artikelauswahl.

Artikelauswahl und Weitergehen nach Gutscheineinlösung folgen der empfehlungsbasierten Route des ursprünglichen Shops. Ohne nutzbares Upsell-Angebot und ohne Cross-Selling geht es direkt zur Ticketpersonalisierung durch die normalen Login-/Session-Prüfungen; mit Cross-Selling zum Warenkorb. Ein nutzbares Upsell-Angebot öffnet zuerst den Dialog. Überspringen führt ohne Cross-Selling zur Personalisierung, mit Cross-Selling zum Warenkorb. Ein bestätigtes Upgrade führt zum Warenkorb; Schließen des Dialogs lässt die Auswahl stehen. Die Navigation schreibt keine Registrierung im Hintergrund.

Im Shop sichtbare Sortimente können eigene Direkteinstiege erhalten. Ist ein Sortiment festgelegt, zeigt die Startseite nur dessen Ticket-Aktion – weder die volle Liste noch die Gutschein-Aktion.

startMode: "home" öffnet die Veranstaltungsstartseite. Besucher wählen dort Tickets kaufen oder Gutschein einlösen; das bloße Öffnen legt keinen Warenkorb an und löst keinen Code ein. Tickets kaufen öffnet die Artikelauswahl (gegebenenfalls auf assortment beschränkt), Gutschein einlösen die Code-Maske. Ohne eventSlug erscheint zuerst die Veranstaltungsübersicht und danach die Startseite des gewählten Events. Ein vorbereiteter Gutschein-Warenkorb oder eine wiederaufnehmbare Journey hat Vorrang vor einem neuen Einstieg.

Standard

Artikelstart

Nur eventSlug: Das Micro-Frontend öffnet die Artikelauswahl dieser Veranstaltung mit allen im Shop sichtbaren Sortimenten.

assortment

Nur ein Sortiment

assortment beschränkt den Artikelstart auf ein Sortiment der Veranstaltung: Id oder exakter Name (Groß-/Kleinschreibung egal). Versteckte Sortimente sind erlaubt, so lassen sich z. B. kostenlose Pressetickets auf einer eigenen Seite anbieten. Ein Wert ohne Treffer zeigt eine sichtbare Fehlermeldung statt still alles zu listen.

startMode: "voucher"

Gutschein einlösen / Ticket registrieren

Die Journey öffnet statt der Artikelliste eine Code-Maske. Ein gültiger Code reserviert sein Ticket. Nach erfolgreicher Einlösung bestimmt die Eventeinstellung configuration.redirectAfterRegistration in ADITUS, ob der Besucher auf der Erfolgsseite bleibt oder automatisch weitergeleitet wird. Das Weitergehen folgt derselben empfehlungsbasierten Route wie die Artikelauswahl, vor der Personalisierung durch die normalen Login-/Session-Prüfungen. Zurück erhält die Reservierung. „Weitere Gutscheine hinzufügen“ öffnet die Code-Eingabe auf der Gutscheincode-Seite für denselben Warenkorb erneut; „Tickets auswählen“ öffnet die Artikelauswahl. Ein abgelehnter Code bleibt mit Begründung in der Maske. Ein vollständig abgedecktes Ticket überspringt die Zahlung.

Ein Promotioncode wechselt zur Warenkorb-bepreisten Artikelauswahl und listet alle geänderten Preise. Der Hinweis bleibt nun im Fußbereich neben dem weiterhin nutzbaren Codefeld: Shopper können weitere Promotioncodes auf denselben Warenkorb anwenden, einen Gutschein zusammen mit ihren Mengen einlösen und aktive Promotioncodes einzeln entfernen. Spätere Auswahlrunden lösen alle verbleibenden Codes vor dem Hinzufügen der Artikel erneut ein.

Wo einstellen

Server-Session oder Client-Datensatz

Server-zu-Server erstellte Sessions geben startMode / assortment im Body neben eventSlug mit. Im Browser erstellte Sessions (nur Publishable Key) nehmen alle drei Werte aus dem Client-Datensatz, den dein ADITUS-Kontakt konfiguriert; Body-Werte werden dort ignoriert. Nur assortment braucht eventSlug (400 start_option_requires_event_slug); startMode funktioniert auch mit der Veranstaltungsauswahl; unbekannte Werte liefern 400 invalid_start_mode / invalid_assortment. GET /shop/session-context gibt startMode und assortment der Session zurück.

mint-entry.ts
// Server-Seite: Verwende dieselbe Session-Route wie oben, nur der Body variiert.
// user und die Login-/Session-Route deines Backends baust du selbst.
// eventSlug legt das Event fest. startMode geht auch ohne Slug (Eventauswahl
// zuerst, dann der Modus); nur assortment braucht den Slug
// (sonst 400 start_option_requires_event_slug).

// 1) Artikelstart (Standard): Artikelauswahl des Events
const bodyArticles = { publicKey, email: user.email, eventSlug: "ExperienceDaysv52024" };

// 2) Sortimentsstart: nur ein Sortiment (Id oder exakter Name, Groß-/Kleinschreibung egal)
const bodyAssortment = {
  publicKey, email: user.email,
  eventSlug: "ExperienceDaysv52024",
  assortment: "Pressetickets",
};

// 3) Gutscheinstart: zuerst "Gutschein einlösen / Ticket registrieren"
const 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
/* Gutschein-Maske: dieselben Tokens und Rollen-Hooks wie der Rest der Journey */
.aditus-shop__voucher-panel  { /* Panel um Hinweise + Formular (general panel hook) */ }
.aditus-shop__voucher-hints  { /* die beiden Hinweiszeilen */ }
.aditus-shop__voucher-form   { /* Formular */ }
.aditus-shop__voucher-input  { /* Codefeld (zusätzlich .aditus-shop__field-input) */ }
.aditus-shop__voucher-redeem { /* "Einlösen"-Button (data-variant="primary") */ }
.aditus-shop__field-error    { /* Ablehnungsgrund, role="alert" */ }
/* Veranstaltungsstartseite und Event-Aktionen: strukturierte Expert-Hooks */
.aditus-shop__home             { /* Startseite; Root data-step="home" */ }
.aditus-shop__home-info        { /* Event-Informationen */ }
.aditus-shop__home-info-head   { /* Überschrift und Logo */ }
.aditus-shop__home-info-name   { /* Eventname */ }
.aditus-shop__home-info-date   { /* Datum */ }
.aditus-shop__home-info-venue  { /* Veranstaltungsort */ }
.aditus-shop__home-info-desc   { /* Beschreibung */ }
.aditus-shop__home-actions[data-variant="tiles"] { /* Kacheln, alternativ compact */ }
.aditus-shop__home-action[data-kind="articles"] { /* Tickets kaufen; auch home / voucher / assortment */ }
.aditus-shop__home-action-icon    { /* Icon */ }
.aditus-shop__home-action-body    { /* Textgruppe */ }
.aditus-shop__home-action-title   { /* Beschriftung */ }
.aditus-shop__home-action-desc    { /* optionale Beschreibung */ }
.aditus-shop__home-action-chevron { /* Pfeil */ }
/* Phase-Hook: [data-step="voucher"] auf dem Root */

Gutscheine werden oft als Link auf deine Seite verschickt, mit dem Code als Query-Parameter, z. B. …/tickets?code=ED24-ADITUS. Das Micro-Frontend kann diesen Code übernehmen und einlösen, ohne dass jemand etwas tippen muss. Es gibt zwei Übergabewege; beide münden im selben Einlöse-Aufruf und verhalten sich danach identisch.

voucherCodeParam: "code"

A) Im Browser: das Micro-Frontend liest die URL

Benenne den Parameter in der mount()-Config (voucherCodeParam) oder am Element (voucher-code-param). Beim Mount liest der Adapter ihn einmal, entfernt ihn per history.replaceState aus der Adresszeile (kein Reload, alles andere in der URL bleibt) und löst den Code ein. Ein Reload, Lesezeichen oder weitergeleiteter Link löst deshalb nie doppelt ein. Passend für statische Seiten und die im Browser erstellten Session.

voucherCode im Session-Body

B) Auf dem Server: Code mit der Session übergeben

Dein Backend liest den Parameter aus dem eingehenden Request und legt ihn als voucherCode in den Session-Body (eventSlug optional). Der Code wird Teil der Session und erreicht das Micro-Frontend nur über den authentifizierten GET /shop/session-context, der ihn nur einmal herausgibt und dann löscht; die Session-Antwort gibt den angenommenen Code zur Kontrolle zurück. Kommt der Code aus einem Link, setze zusätzlich voucherCodeParam, damit der Parameter auch aus der Adresszeile verschwindet; bei Eingabe in deiner eigenen Oberfläche entfällt das. Passend, wenn deine Seite ohnehin serverseitig gerendert wird oder du eingehende Links selbst prüfen, protokollieren oder drosseln willst. Liefern beide Wege einen Code, gewinnt der serverseitige.

Was mit dem Code passiert

Gleiches Verhalten auf beiden Wegen

Der Code wird genau einmal und nur in einer frischen Journey eingelöst (eine nach Reload oder Zahlungsrückkehr fortgesetzte Journey ignoriert ihn). Bei startMode "voucher" verhält sich die Code-Maske wie unter „Gutschein einlösen / Ticket registrieren“ beschrieben. Beim Artikelstart wird der Code auf den ersten Warenkorb angewendet, während Artikelansicht, Scrollposition und Mengen erhalten bleiben. „Weiter“ fügt diese Mengen demselben Warenkorb hinzu. Das Micro-Frontend gibt den Code nie an onEvent weiter. Ohne eventSlug wird er nach der Eventauswahl eingelöst.

voucher-link.ts
// Gutschein-Links zeigen auf deine Seite: https://deine-seite.de/tickets?code=ED24-ADITUS
// Route, Request und Authentisierung unten sind Beispiele für deine eigene Website.
// Der Parametername ist frei: hier "code".

// Variante A (Browser): das Micro-Frontend liest den Parameter selbst.
import { mount } from "@aditus/shop-embed";

mount(document.getElementById("aditus-shop")!, {
  publicKey: "pk_deine_seite",
  sessionToken,                 // Session mit eventSlug (Artikel- oder Gutscheinstart)
  voucherCodeParam: "code",     // ?code=… einmal lesen, aus der Adresszeile entfernen, einlösen
});
// Web Component: <aditus-shop public-key="pk_deine_seite" voucher-code-param="code"></aditus-shop>

// Variante B (Server): dein Backend übernimmt den Code beim Erstellen der Session.
// (Express-Beispiel; der Code wandert nie in die Session-Antwort, sondern in den Session-Kontext.)
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_deine_seite",
      email: req.user?.email,
      eventSlug: "ExperienceDaysv52024",
      startMode: "voucher",       // oder weglassen: Artikelstart, Code landet im ersten Warenkorb
      voucherCode: code,          // undefined = normaler Start ohne Code
    }),
  });
  const { sessionToken }: { sessionToken: string } = await r.json();
  // Seite mountet zusätzlich mit voucherCodeParam: "code", damit ?code= aus der Adresszeile
  // verschwindet (der Session-Code gewinnt, eingelöst wird nur einmal).
  res.render("tickets", { sessionToken });
});

Optional: Gutschein schon beim Erstellen der Session reservieren

Soll deine Website einen abgelehnten Gutschein vor dem Öffnen des Embeds selbst behandeln, setze redeemVoucherOnMint: true beim vertrauenswürdigen POST /api/shop/session. Anders als die normale voucherCode-Übergabe legt das einen Warenkorb für den authentifizierten Nutzer an und reserviert den Gutschein dort wirklich, bevor die Session-Erstellung erfolgreich ist – kein zusätzlicher Vorab-Prüfrequest. Bei Erfolg gib nur den erhaltenen sessionToken an den Browser weiter. Das Embed übernimmt DENSELBEN Warenkorb ohne Neuanlage oder zweite Einlösung, sowohl bei startMode voucher als auch articles. Ohne die Option gilt die normale voucherCode-Übergabe.

Dieser Modus braucht voucherCode, einen festen eventSlug, einen identifizierten Nutzer (email; nicht anonym) und den Header Idempotency-Key. Er ist nur mit Mint-Secret auf deinem Backend verfügbar, nicht über POST /shop/embed-session im Browser. Er unterstützt Gutschein-/Ticketcodes, keine PromotionCodes. Der Code wird im Warenkorb reserviert, nicht bereits als Ticket registriert oder ausgegeben: Registrierung und Abschluss folgen. Spätere Prüfungen der Shop-API können noch scheitern; eine erfolgreiche Session-Erstellung garantiert keine fertige Ticketausgabe.

prepare-voucher.ts
// Server-Seite deiner Website: ein Gutschein-Versuch, kein zusätzlicher Prüf-Call.
// Authentisierung und Speicherung dieses Versuchs implementierst du selbst.
// attemptKey ist eine von deinem Backend pro logischem Einlöseversuch dauerhaft gespeicherte
// Zufalls-ID (z. B. crypto.randomUUID()). Bei Netzwerkfehlern dieselbe ID
// und DENSELBEN Body erneut senden, nie vor einem Retry neu erzeugen.
async function 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_deine_seite",
      email: user.email, // nur aus deiner authentifizierten Server-Session
      eventSlug: "ExperienceDaysv52024",
      startMode: "voucher", // oder "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) {
    // Erst jetzt an den Browser reichen und das Embed mounten.
    return { sessionToken: result.sessionToken, expiresAt: result.expiresAt };
  }
  if (response.status === 422 && result.error === "voucher_rejected") {
    // Eigene Oberfläche: fachliche Ablehnung mit reason behandeln, kein Embed öffnen.
    // Nur freigegebene reason-Werte in eigene Texte übersetzen.
    return { rejected: result.reason };
  }
  if (response.status === 409 && result.error === "voucher_preparation_uncertain") {
    // Unklar bleibt unklar; kein automatischer Erfolg durch erneutes POST.
    // Kann die Shop-API die Cart-ID nicht liefern, ist manuelle Klärung nötig.
    throw new Error("Einlösung unklar; mit ADITUS klären, nicht neu buchen.");
  }
  if (response.status === 410 && result.error === "voucher_preparation_expired") {
    throw new Error("Vorbereitete Session nicht mehr gültig; keine blinde neue Session.");
  }
  // 400 = ungültiger Request/Key, 409 = Konflikt/laufender Versuch,
  // 502/503 voucher_upstream_error = Infrastruktur, kein Fachfehler.
  throw new Error(`Session-Erstellung nicht abgeschlossen (${response.status})`);
}

422 voucher_rejected mit bereinigtem reason ist eine fachliche Ablehnung: Zeige sie auf deiner Website und mounte ohne Token kein Embed. Ungültige Konfiguration/Key ergibt 400; 409 idempotency_conflict bedeutet gleicher Key mit anderen Eingaben, während ein laufender/unklarer Versuch kein ungültiger Gutschein ist. 502/503 und Netzwerk-Timeouts sind Infrastruktur-/unklare Ergebnisse, keine Gutscheinablehnung. Speichere einen neuen zufälligen Idempotency-Key pro logischem Versuch auf deinem Server und wiederhole nur mit demselben Key und exakt denselben Eingaben. Nach unklarem Ausgang niemals mit neuem Key wiederholen: Die Reservierung könnte bereits existieren. Code, E-Mail, rohe API-Fehler und Session-Token nicht protokollieren.

Eine erfolgreiche Session-Erstellung kann einen reservierten Warenkorb hinterlassen, selbst wenn der Käufer die Seite nie öffnet. Das Aufräumen eines bekannten Warenkorbs nach Ablehnung/Widerruf erfolgt bestmöglich; ein Sweeper beim Start und danach alle 60 Sekunden wiederholt bis zu 25 Datensätze je Lauf. Gleicher Key und gleiche Eingabe liefern bei gültigem abgeschlossenen Versuch dasselbe Ergebnis. Ist der ursprüngliche Token abgelaufen, widerrufen, abgeschlossen, gelöscht oder übertragen, kommt stattdessen 410 voucher_preparation_expired – die Reservierung nicht blind neu anlegen. Ein berechtigter Nachfolger kann die Vorbereitung beim ersten Warenkorbzugriff verlängern oder übernehmen. Den Code aus der URL deiner Seite zu entfernen, gibt keine Reservierung frei.

Genaue Fehler bei der Session-Erstellung: 400 invalid_redeem_voucher_on_mint / voucher_preparation_requires_user_event_code / invalid_idempotency_key (oder normale Validierung); 422 voucher_rejected enthält reason und upstreamStatus, niemals den rohen Fehlercode oder -text der Shop-API. Mögliche Gründe: unknown · articleLimit · exhausted · ownership · used · reserved · limit · cartLimit · locked · canceled · otherEvent · legitimation · rejected · unsupported_code_type.

409 idempotency_conflict bedeutet andere Eingaben; voucher_preparation_pending liefert Retry-After: 2 Sekunden, voucher_preparation_uncertain bleibt auch bei Wiederholung unklar (kein automatisch späterer Erfolg). 410 voucher_preparation_expired heißt: die ursprüngliche Vorbereitung ist nicht mehr abrufbar. 502/503 voucher_upstream_error bedeutet einen Fehler im ADITUS-Ticketsystem. Hat die Shop-API einen Warenkorb angelegt, aber dessen ID nicht zurückgeliefert, sind automatischer Abgleich und Freigabe mit der verfügbaren API unmöglich: der unklare Datensatz bleibt erhalten und der Server legt beim Retry keinen zweiten Warenkorb an. Vor einem neuen Versuch ist eine manuelle Klärung mit ADITUS nötig. Wenn session-context nicht geladen werden kann, zeigt das Embed einen Fehler mit Wiederholungsoption statt still einen frischen Warenkorb anzulegen.

Probiere alle drei Varianten live im Integrationstester: Session mit eventSlug erstellen, startMode oder Sortiment wählen, mounten und das Request-Protokoll beobachten. Das Demo-System akzeptiert den Test-Gutschein ED24-ADITUS auf ExperienceDaysv52024 (jede Einlösung reserviert das Ticket rund 20 Minuten; Zurückgehen gibt es wieder frei). Integrationstester öffnen

Optional: anonym browsen, beim Checkout anmelden

Du musst nicht von Anfang an wissen, wer der Käufer ist. Mounte mit einer Gast-Session, und das Micro-Frontend lässt einen anonymen Besucher Events durchstöbern, Artikel wählen und den Warenkorb füllen. Erst wenn er den Warenkorb verlässt (Richtung Registrierung und Checkout) bittet es dich, den echten Nutzer festzulegen, über den Callback onUserRequired. Der restliche Ablauf läuft dann mit erhaltenem Warenkorb weiter.

  1. 1

    Mit einer Gast-Session mounten

    Ein Session-Token ist weiterhin nötig, um den Shop zu aktivieren. Es kann aber eine Gast-Session sein: einfach ohne das Feld email erstellen, keine Platzhalter-Adresse nötig. Der Besucher browst und füllt den Warenkorb darunter.

  2. 2

    Das Verlassen des Warenkorbs löst onUserRequired aus

    Sobald der Besucher den Warenkorb verlässt, ruft das Micro-Frontend deinen Callback einmal auf und zeigt einen neutralen Wartezustand. Authentifiziere den Nutzer (Login/SSO) und erstelle dann eine frische, nutzergebundene Session genau wie die erste: server-zu-server mit deinem Secret. Auch das spätere Festlegen des Nutzers läuft über diese serverseitige Session-Erstellung. Das Secret erreicht nie den Browser und der Nutzer wird nie im Client-Code gesetzt. Dein Callback reicht nur den entstandenen opaken Token weiter. Entscheidend: Dein Backend leitet diese Identität aus seiner eigenen authentifizierten Session ab (Cookie/JWT); es darf E-Mail oder User-ID niemals aus dem Browser entgegennehmen, sonst könnte ein böswilliger Besucher eine fremde Session anfordern und über den Prefill deren personenbezogene Daten lesen.

  3. 3

    Gib den neuen Token zurück. Der Warenkorb wandert mit

    Gib den frischen Session-Token zurück; das Micro-Frontend übernimmt ihn und bindet den identifizierten Nutzer an den bestehenden Warenkorb. Warenkorb, Artikel und etwaige Zeitfenster bleiben unverändert erhalten. Danach geht es zur Registrierung. Gib null zurück, um abzubrechen. Der Besucher bleibt einfach im Warenkorb, ohne Fehler.

  4. 4

    Ohne onUserRequired: fixer Nutzer

    Lässt du den Callback weg, ändert sich nichts am klassischen Ablauf: Mounte mit einer nutzergebundenen Session, und das Micro-Frontend agiert vom ersten Schritt an als dieser Nutzer.

page.ts
import {
  mount,
  type ShopHandle,
  type AditusShopConfig,
} from "@aditus/shop-embed";

const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount-Ziel #aditus-shop nicht gefunden");

// Ein Gast-Session-Token reicht zum Mounten: server-zu-server erstellt wie
// gehabt, nur ohne das Feld email (keine Platzhalter-Adresse nötig). Der
// Besucher darf browsen und den Warenkorb füllen, ohne dass ein konkreter
// Nutzer feststeht.
const config: AditusShopConfig = {
  apiBase: "https://developers.aditus.com/api/shop",
  publicKey: "pk_deine_seite",
  sessionToken: guestSessionToken,

  // Wird einmal aufgerufen, sobald der Besucher den Warenkorb Richtung
  // Registrierung verlässt. Hier authentifizierst du den Nutzer (Login/SSO)
  // und erstellst server-zu-server eine neue, nutzergebundene Session.
  // Gib den frischen Token zurück -> das Micro-Frontend übernimmt ihn und baut
  // den Warenkorb unter dem identifizierten Nutzer neu auf (Artikel bleiben
  // erhalten). Gib null zurück, um abzubrechen -> der Besucher bleibt im
  // Warenkorb, ohne Fehler.
  onUserRequired: async () => {
    // Ruft nur dein Backend auf – ohne Identität im Payload. Dein Backend liest
    // den eingeloggten Nutzer aus seiner eigenen Session (Cookie/JWT) und erstellt
    // server-seitig für genau diese Identität. Niemals die E-Mail aus dem Browser
    // übergeben – sonst könnte ein Angreifer hier eine fremde Adresse einsetzen
    // und über den Prefill an deren personenbezogene Daten gelangen.
    // host.loginAndMintSession(): Promise<string | null> ist deine eigene Funktion.
    // Implementiere Login/SSO + authentisierten Backend-Call + serverseitige Session-Erstellung auf deiner Website.
    // Bei Abbruch liefert die Funktion null; bei Fehler wirft sie explizit.
    return host.loginAndMintSession();
  },
};

const handle: ShopHandle = mount(el, config);

Die Anmeldung läuft über deine Website

ADITUS authentifiziert deine Endnutzer nicht. Deine Seite ist die alleinige Quelle der Wahrheit, wer der Nutzer ist. Du betreibst dein eigenes Login, SSO oder deine Session. ADITUS vertraut lediglich einem Session-Token, das dein Backend mit einem Secret-Key ausstellt. Die Vertrauenskette lautet: dein Auth beweist die Identität, dein mit dem Secret ausgestellter Token verbürgt sie, das Micro-Frontend präsentiert sie.

  1. 1

    Deine Seite authentifiziert den Nutzer

    Login, SSO und Mitglieder-Session liegen komplett in deiner Hand. ADITUS ist hier nicht beteiligt.

  2. 2

    Dein Backend erstellt eine Session

    Server-zu-Server mit dem Secret-Key. Du übergibst, wer das Micro-Frontend sein soll (E-Mail), und bindest es an deinen publicKey.

  3. 3

    Die Seite mountet das Micro-Frontend

    Der opake Token wird an den Browser gereicht und als sessionToken an mount() übergeben. Der Secret bleibt auf deinem Server.

  4. 4

    Jeder Call trägt die Session

    Das Micro-Frontend sendet bei jeder Anfrage X-Aditus-Session; die Shop-API ermittelt den Warenkorb-User aus der Session.

  5. 5

    Kein Token → Micro-Frontend bleibt inaktiv

    Ohne Session wird das Micro-Frontend nie aktiv: es zeigt einen neutralen Hinweis und macht keine Calls. Weder Warenkorb noch Checkout sind verfügbar. Erstelle eine Session, um es einzuschalten.

1. Session auf deinem Backend erstellen

server.ts
// Server-Seite deiner Website. Der Secret-Key bleibt auf dem Server und gelangt nie in den Browser.
// user und die Login-/Session-Route zwischen Browser und deinem Server baust du selbst, ADITUS liefert sie nicht.
// Dein Backend hat den Nutzer bereits selbst authentifiziert (Login/SSO/Session).
// Wichtig: user stammt aus dieser Server-Session, nie aus dem Request-Body des
// Browsers. Sonst könnte ein Angreifer eine fremde E-Mail unterschieben.
const res = await fetch("https://developers.aditus.com/api/shop/session", {
  method: "POST",
  headers: {
    // Mint-Secret deines Clients (im ADITUS-Admin erzeugt), nur server-seitig.
    Authorization: `Bearer ${process.env.ADITUS_MINT_SECRET}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    publicKey: "pk_deine_seite",      // bindet die Session an deinen Key (Pflicht)
    email: user.email,                 // wer das Micro-Frontend sein soll (weglassen = anonyme Gast-Session)
    externalUserId: user.id,           // optionale Metadaten (deine User-ID)
    // sharedCartKey: visitor.stableRef, // Pflicht für anonyme shared-Carts (Standard):
    // opake Referenz aus deiner vertrauenswürdigen Server-Session, nie Browser-Eingabe
    ttlSeconds: 1200,                  // optional: Lebensdauer (Default 20 min, max 24 h)
    eventSlug: "ExperienceDaysv52024", // optional: Einstieg direkt in dieses Event (sonst Eventübersicht)
    // startMode: "voucher",           // optional: Start mit der Gutschein-/Ticketcode-Maske (ohne eventSlug nach der Eventauswahl)
    // assortment: "Pressetickets",    // optional (mit eventSlug): nur dieses Sortiment (Id oder Name)
    // voucherCode: req.query.code,    // optional: Code aus einem Gutschein-Link im Embed einlösen
    // redeemVoucherOnMint: true,      // optional: stattdessen hier reservieren (benötigt email, eventSlug und Idempotency-Key)
  }),
});

// Wähle den Antwort-Typ frei und reiche sessionToken an den Browser (inline oder fetch).
const { sessionToken }: { sessionToken: string; expiresAt: number } = await res.json();

2. Mit dem Token mounten

page.ts
import {
  mount,
  type ShopHandle,
  type AditusShopConfig,
} from "@aditus/shop-embed";

// getElementById kann null sein. Prüfe dies in TypeScript sauber statt mit "!".
const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount-Ziel #aditus-shop nicht gefunden");

// AditusShopConfig typisiert alle Optionen und bietet Autovervollständigung.
const config: AditusShopConfig = {
  apiBase: "https://developers.aditus.com/api/shop",
  publicKey: "pk_deine_seite", // Pflicht, sobald eine Session im Spiel ist
  sessionToken,                // vom Backend ausgestellt, nur im Speicher halten
  cartScope: "shared",          // Standard; vertrauenswürdige Server-Identität erforderlich
};

const handle: ShopHandle = mount(el, config);

// Das Micro-Frontend sendet bei jedem Shop-Call den Header X-Aditus-Session.
// Beim Logout die Session serverseitig widerrufen und neu mounten/aktualisieren.

3. Beim Logout widerrufen

server.ts
// Server-Seite, z. B. beim Logout deines Nutzers.
// Die Logout-Route zwischen Browser und deinem Server baust du selbst; nur dein Backend ruft ADITUS auf.
await fetch(`https://developers.aditus.com/api/shop/session/${sessionToken}`, {
  method: "DELETE",
  headers: { Authorization: `Bearer ${process.env.ADITUS_MINT_SECRET}` },
});
// { revoked: true }

Die fertige Bestellung kommt zurück an deine Seite

Wenn ein Ticketkauf abgeschlossen ist, brauchst du keine separate Order-Abfrage. Weil das Micro-Frontend nativ in deiner Seite läuft (Light-DOM, kein iFrame), reicht es die fertige Bestellung direkt per onComplete-Callback an deinen Code zurück. Dies geschieht über einen direkten JS-Aufruf, nicht über postMessage. Das Micro-Frontend hat die Bestellung bereits für dich aufgelöst: du bekommst die Bestellnummer und die Ticket-Links, nicht nur eine 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-Ziel #aditus-shop nicht gefunden");

const handle: ShopHandle = mount(el, {
  publicKey: "pk_deine_seite",
  sessionToken,
  // Wird genau einmal aufgerufen, sobald die Bestellung platziert ist.
  // Das Micro-Frontend hat die Bestellung bereits für dich aufgelöst. Du bekommst
  // die aufgelöste Bestellung, nicht nur eine ID.
  onComplete: (order: CompletedOrder) => {
    order.id;       // Order-GUID für die Weiterverarbeitung (Server, Abgleich)
    order.number;   // Bestellnummer für die Anzeige
    order.status;   // Status der Bestellung
    order.buyer;    // Käufer (Name / E-Mail), falls vorhanden
    order.tickets;  // [{ name, links: [{ kind: "pdf" | "apple" | "google", url }] }]

    // -> order.id speichern, Bestätigung zeigen, DOI / Newsletter anstoßen ...
  },
  // Wird bei jedem nicht behebbaren Fehler im Ablauf aufgerufen.
  onError: (err: Error) => {
    console.error("Shop-Fehler:", err.message);
  },
});

Was du bekommst

onComplete feuert einmal pro erfolgreichem Checkout mit der aufgelösten Bestellung: id (die Order-GUID, dein Schlüssel für die Weiterverarbeitung), number (die Bestellnummer für die Anzeige), status, buyer (Name & E-Mail, falls vorhanden) und tickets. Jedes Ticket trägt typisierte Links mit kind „pdf“, „apple“ oder „google“. onError feuert bei jedem nicht behebbaren Fehler im Ablauf.

Zahlarten mit Redirect

Zahlarten, die auf der Seite bleiben (Rechnung, Vorkasse), feuern onComplete sofort. Eine Zahlart, die den Browser auf eine externe Bezahlseite schickt (z. B. Saferpay), übernimmt das Micro-Frontend komplett: Es leitet die Rücksprung-URLs aus der Seite ab, auf der es läuft, schickt den Käufer hinaus und schließt die Bestellung beim Rücksprung selbst ab, inklusive onComplete, ganz ohne Code auf deiner Seite. Nur wenn der Rücksprung auf einer anderen Seite landen soll, überschreibst du redirectUrls.successUrl / cancelUrl / errorUrl.

GA4- / E-Commerce-Events (optional)

Das Micro-Frontend kann den Einkaufs-Funnel an deine Analytics melden, aber nur, wenn du es darum bittest. Es bündelt kein gtag, kein GTM und kein GA-SDK, lädt nichts, setzt keine Cookies und sendet von sich aus nichts. Du ergänzt einen optionalen Callback, onEvent, und bekommst typisierte Events, die bereits im GA4-Enhanced-Ecommerce-Schema von Google geformt sind. Lässt du onEvent weg, ändert sich nichts. Welches Tool du fütterst und ob du senden darfst (Consent), bleibt komplett bei dir.

mount.ts
import {
  mount,
  type ShopHandle,
  type ShopAnalyticsEvent,
} from "@aditus/shop-embed";

const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount-Ziel #aditus-shop nicht gefunden");

const handle: ShopHandle = mount(el, {
  publicKey: "pk_deine_seite",
  sessionToken,
  // Optional. Lässt du onEvent weg, ändert sich nichts: das Micro-Frontend
  // sendet nichts, lädt kein gtag/GTM/GA-SDK und setzt keine Cookies.
  // Tool, Consent und Mapping bleiben komplett bei dir. Dieses Beispiel nutzt GA4 via gtag.
  onEvent: (event: ShopAnalyticsEvent) => {
    // Consent liegt bei dir: erst senden, wenn der Nutzer zugestimmt hat.
    if (!hasAnalyticsConsent()) return;
    // Die Events sind bereits im GA4-Schema (name + items/value/currency ...),
    // also 1:1 an gtag durchreichen.
    const { name, ...params } = event;
    window.gtag?.("event", name, params);
  },
});

Bereits im GA4-Format

Jedes Event trägt einen GA4-Event-Namen plus seine Standard-Parameter: items (mit item_id, item_name, price, quantity, item_category und (wo zutreffend) discount und coupon), value und currency; add_payment_info ergänzt payment_type, purchase ergänzt transaction_id. Nimm name per Destructuring heraus und reiche den Rest direkt an gtag("event", name, params). Ein Umbauen ist nicht nötig.

Consent bleibt bei dir

onEvent ist einfach eine ganz normale JavaScript-Funktion in deiner Seite, die wir direkt aufrufen. Das Micro-Frontend selbst sendet keine Analytics irgendwohin. Nichts verlässt die Seite, bis dein Handler sendet. Dein Consent-Management behält also die volle Kontrolle: koppel das Weiterreichen an den Consent, route in den dataLayer von GTM statt an gtag, batche oder verwirf Events ganz. Ein Fehler, den dein Handler wirft, wird abgefangen und isoliert und bricht den Benutzer-Flow nie. Halte den Handler leichtgewichtig (ein gtag-/dataLayer-Push), da er inline läuft.

EventFeuert, wenn
view_item_listDer Benutzer sieht die Artikelliste (feuert einmal pro angezeigtem Set).
add_to_cartEin Artikel oder Add-on wird in den Warenkorb gelegt.
remove_from_cartEine Warenkorb-Position oder ein Add-on wird entfernt.
view_cartDie Warenkorb-Ansicht wird angezeigt (einmal pro Warenkorb).
begin_checkoutDer Benutzer verlässt den Warenkorb Richtung Registrierung / Checkout.
add_payment_infoEine Zahlart wird gewählt (enthält payment_type).
purchaseDie Bestellung ist platziert (enthält transaction_id, value, items).

Event-Bus: handle.on / handle.off (optional)

Neben den Config-Callbacks trägt das von mount() zurückgegebene Handle einen kleinen, komplett optionalen Event-Bus. Du abonnierst mit handle.on(event, listener) (der Aufruf gibt die passende Abmeldefunktion zurück) und meldest dich mit handle.off(event, listener) ab. Rufst du on() nie auf, ändert sich nichts: Der Bus bringt keine Abhängigkeiten mit, sendet nichts und kostet nichts. Er ist reine Beobachtung für deine Seite (ein Mini-Cart-Badge, ein Bestätigungsbanner, dein eigenes Logging) und ergänzt die Callbacks, statt sie zu ersetzen.

mount.ts
import {
  mount,
  type ShopHandle,
  type ShopCartUpdate,
} from "@aditus/shop-embed";

const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount-Ziel #aditus-shop nicht gefunden");

const handle: ShopHandle = mount(el, {
  publicKey: "pk_deine_seite",
  sessionToken,
});

// Optional. Ohne on() funktioniert das Micro-Frontend ganz normal.
// Der Bus ist reine Beobachtung, kein Event ist Pflicht.
// on() liefert die Abmeldefunktion zurück; alternativ handle.off(name, fn).
const offCart = handle.on("cart:update", (cart: ShopCartUpdate) => {
  // Bei jeder Warenkorb-Änderung: Anzahl, Summe, Positionen (GA4-Item-Form).
  updateMiniCartBadge(cart.itemCount); // z. B. Badge im Seiten-Header
});

handle.on("checkout:complete", ({ order }) => {
  // Die Bestellung ist platziert. Du erhältst dieselbe aufgelöste Bestellung wie in onComplete.
  showConfirmationBanner(order.number);
});

handle.on("analytics", (event) => {
  // Spiegel des GA4-Funnels (identische Payloads wie config.onEvent).
  console.debug("Funnel:", event.name);
});

// Später gezielt abmelden; der Rest bleibt aktiv:
offCart();
EventFeuert, wenn
cart:updateDer Warenkorb ändert sich: angelegt, Position hinzugefügt oder entfernt, oder geleert. Enthält cartId, itemCount, value, currency und die Positionen als GA4-geformte items.
phase:changeDer sichtbare Schritt hat gewechselt. Das Event enthält phase (events · home · articles · voucher · cart · registration · legitimation · buyer · payment · summary · done, gleiches Vokabular wie data-step), previous (null beim Einstiegsschritt) und startPhase, den Einstiegsschritt der Session. Damit steuert deine Seite eigene Zurück-/Neu-starten-Buttons, ohne CSS-Hooks auszulesen.
checkout:completeDie Bestellung ist platziert. Feuert direkt nach config.onComplete mit demselben aufgelösten Order-Objekt, bevor das Micro-Frontend seine Abschlussseite zeigt. Rufe synchron preventDefault() auf, um stattdessen deine eigene Abschlussseite zu zeigen (siehe „Eigene Abschlussseite“).
analyticsSpiegel des GA4-Funnels oben: jede onEvent-Payload wird unverändert auch auf dem Bus emittiert.

Listener überleben update()

Abos hängen am Handle, nicht an einem Render: handle.update({ ... }) rendert das Micro-Frontend neu, lässt aber jeden Listener angeschlossen. Der Unmount via handle() löst alle Listener automatisch. off() (oder die zurückgegebene Abmeldefunktion) brauchst du nur, wenn du aufhören willst zuzuhören, während der Shop weiterläuft.

Isoliert, nie blockierend

Ein Listener, der einen Fehler wirft, wird abgefangen und isoliert. Er bricht nie den Benutzer-Flow und stört keine anderen Listener. Das Event „analytics“ spiegelt den GA4-Funnel mit Payloads identisch zu config.onEvent; du kannst den Funnel also über den Bus, den Callback oder beides konsumieren. Dabei gilt dieselbe Consent-Regel: Nichts verlässt die Seite, bis dein Code sendet.

Eigene Abschlussseite

checkout:complete feuert, bevor das Micro-Frontend seine eigene Abschlussseite zeigt. Zeigt deine Seite eine eigene Bestätigung, rufst du im Listener synchron preventDefault() auf: event.preventDefault() am Element <aditus-shop>, preventDefault() aus dem Payload bei handle.on() oder completion.preventDefault() aus dem zweiten Argument von config.onComplete. Das Micro-Frontend zeigt dann keine Abschlussseite, scrollt nicht und rendert keinen weiteren Schritt, deine Seite übernimmt. Die Bestellung ist in jedem Fall platziert. Der Aufruf muss im Listener selbst passieren, nicht nach einem await. Ohne ihn zeigt das Micro-Frontend seine Abschlussseite wie bisher. Das gilt auch für den Rücksprung von einer externen Bezahlseite.

Beende die Session danach. Die Bestellung ist abgeschlossen, also kann dein Server die Session im selben Zug widerrufen: DELETE /api/shop/session/:token mit deinem Secret-Key (siehe „Session widerrufen“). Die Bestellung und die Ticket-Links im Order-Objekt bleiben gültig, denn die Links hängen nicht an der Session. Für einen weiteren Kauf erstellst du eine neue Session und bindest das Micro-Frontend neu ein. Das Micro-Frontend beendet die Session nicht selbst, weil der Widerruf deinen Secret-Key braucht. Behalte sie nur, wenn deine Seite bewusst in derselben Session weiterverkauft.

Befehle an das Micro-Frontend: goTo und back

Neben dem Beobachten kann deine Seite dem Micro-Frontend zwei eng begrenzte Anfragen schicken. Beide liefern true, wenn der Shop gewechselt hat, sonst false; sie mounten nie neu und verlieren die Journey nicht. goTo("cart") zeigt für deinen eigenen Mini-Warenkorb den Warenkorb-Schritt; angenommen wird das nur auf dem Registrierungsschritt, solange noch nichts geschrieben ist. back() geht einen Schritt zurück, genau wie der Zurück-Button im Shop. Überall, wo dieser Button fehlt oder gesperrt ist, kommt false zurück: auf dem Einstiegsschritt (startPhase), auf der Abschlussseite und während eine Zahlung oder eine andere Änderung läuft. Beide gibt es am Mount-Handle und an <aditus-shop> (element.back(), element.goTo("cart")).

browser-back.ts
import type { ShopHandle } from "@aditus/shop-embed";

// Optional: Die Zurück-Geste des Browsers geht im Shop einen Schritt zurück.
// Das Micro-Frontend schreibt selbst nie in den Browser-Verlauf. Deine Seite
// hält einen eigenen Verlaufseintrag, solange der Shop nach dem Einstieg ist.
export function connectBrowserBack(shop: ShopHandle): () => void {
  let inside = false; // nach dem Einstiegsschritt und vor der Abschlussseite
  let entry = false; // unser Verlaufseintrag liegt oben
  const 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(); // Eintrag wird nicht mehr gebraucht
    }
  });
  const onPopState = () => {
    if (!entry) return; // normale Navigation deiner Seite
    entry = false;
    // false: Der Zurück-Button im Shop fehlt oder ist gesperrt (z. B. Zahlung läuft).
    if (!shop.back() && inside) {
      history.pushState({ aditusShop: true }, "");
      entry = true;
    }
  };
  window.addEventListener("popstate", onPopState);
  return () => {
    offPhase();
    window.removeEventListener("popstate", onPopState);
  };
}

// Eigener Mini-Warenkorb: zurück zum Warenkorb-Schritt, sonst zum Shop scrollen.
export function showCart(shop: ShopHandle, container: HTMLElement): void {
  if (!shop.goTo("cart")) container.scrollIntoView({ behavior: "smooth" });
}

Das Micro-Frontend schreibt selbst nie in den Browser-Verlauf: URLs und Router gehören deiner Website. Das Beispiel hält genau einen zusätzlichen Verlaufseintrag, solange der Shop nach dem Einstiegsschritt ist. So geht die Zurück-Geste des Browsers (auch das Wischen auf dem iPhone) zuerst im Shop einen Schritt zurück und verlässt die Seite erst vom Einstiegsschritt aus. Nutzt deine Website einen clientseitigen Router (React Router, Next.js, Vue Router), legst du diesen Eintrag über den Router an statt mit history.pushState, damit der Router ihn nicht als fremde Navigation behandelt.

publicKey vs. sessionToken

Halte die beiden auseinander. Sie beantworten verschiedene Fragen und werden kombiniert, nicht ausgetauscht.

publicKey: Origin-Vertrauen

Publishable. Er steckt in deinem Client-Code, ist also kein Secret und gewährt für sich genommen nichts. Das Backend erzwingt eine Domain-Whitelist pro Key: ein Key funktioniert nur von seinen registrierten Domains. Er beantwortet „welche Seite ist das?“ Nur eine explizite, bewusst unsichere Client-Option des Betreibers hebt diese Whitelist für einen einzelnen Client auf; Aktivstatus, Session-Pflicht und Secrets gelten auch dann weiter.

sessionToken: Identität

Secret-geminted auf deinem Backend, opak und kurzlebig. Er beantwortet „wer ist der Nutzer?“ Eine Session ist an den publicKey gebunden, für den sie erstellt wurde: eine Session-Anfrage muss diesen Key mitsenden, und eine Abweichung wird abgelehnt (403).

Sicherheitsregeln

  • Der Secret-Key lebt nur auf deinem Server, nie in Client-Code, Bundles oder an den Browser ausgelieferten Env-Dateien.
  • Speichere den Token nicht selbst in localStorage oder Cookies; halte ihn im Speicher und erneuere ihn, indem du eine neue Session erstellst. Für Neuladen und den Zahlungs-Roundtrip legt das Micro-Frontend ihn selbst im sessionStorage des Tabs ab.
  • Tokens sind kurzlebig (Default 20 Minuten, max 24 Stunden) und single-user.
  • Widerrufe die Session beim Logout: Ein widerrufener Token wird sofort abgelehnt (401). Bis zum Ablauf oder Widerruf ist ein gültiger Token ein Bearer-Token. Behandle ihn auch so.
  • Kombiniere die Session immer mit ihrem publicKey. Ein fehlender oder abweichender Key wird abgelehnt (403).
  • Eine ungültige oder abgelaufene Session führt zu 401. Es gibt keinen Rückfall auf einen anderen Nutzer.

Was ein Session-Token kann

Ein Session-Token ist ein Bearer-Token. Das ist der genaue Umfang, den die ADITUS-API dafür durchsetzt. Nutze diese Informationen für dein Threat Model und Security-Review.

Token & Laufzeit
256-Bit-Zufall, opak, trägt keine Daten und ist nicht dekodierbar. Default 20 Minuten, beim Erstellen der Session von 1 Minute bis 24 Stunden konfigurierbar. Eine Session wird nie verlängert. Erneuere sie, indem du eine neue Session erstellst.
Bindung
Gebunden an den publicKey, für den sie erstellt wurde. Jeder Session-Request muss genau diesen Key mitsenden, und der Request-Origin wird gegen die Domain-Whitelist des Keys geprüft. Ein anderer Key oder eine fremde Origin wird abgelehnt (403).
Anonyme Session
Ohne User erstellt: darf Events browsen sowie den eigenen Warenkorb anlegen und bearbeiten. Registrierung, Zeitfenster, Zahlungszuordnung und Checkout werden serverseitig abgelehnt (403), bis deine Seite eine identifizierte Session einsetzt.
Identifizierte Session
Zusätzlich: Registrierung, Zeitfenster, Zahlungszuordnung und Checkout, immer beschränkt auf die Warenkörbe, die dieser Session gehören. Mit protectUserData sind die für den User hinterlegten Käuferdaten geschützt: Ein Schreibzugriff, der sie ändern würde, wird abgelehnt (403).
Warenkorb-Eigentum
Ein Warenkorb ist über einen schlüsselbasierten Hash (HMAC) an seinen Eigentümer gebunden: bei einer anonymen Session an den Token selbst, bei einer identifizierten Session an die von deinem Backend beim Erstellen der Session gesetzte User-Identität, jeweils pro publicKey. Sessions anderer User erhalten keinen Zugriff darauf, und Schreibzugriffe auf einen Warenkorb werden serialisiert.
Personenbezogene Daten
Der Session-Context liefert Flags, IDs und die User-Kennung, die dein eigenes Backend erstellt hat. All das kennt deine Website ohnehin. Käuferdaten sind nur über die eigentumsgeprüften Warenkorb- und Registrierungs-Routen erreichbar.
Rate Limits
Standardwerte: Erstellen und Widerrufen 300 pro Minute je Client und IP, Adress-, Firmen- und Orts-Autocomplete 60 pro Minute je Session und 3000 pro Minute je Client über alle Sessions. Jedes Limit ist ein eigenes Budget deines Clients, andere Clients beeinflussen es nicht. Brauchst du mehr, legt ADITUS individuelle Limits für deinen Client fest.
Audit-Log
Anfragen an die Shop-API werden protokolliert und 90 Tage aufbewahrt: Client, normalisierte Route, Status, Origin-Host, ein gekürzter Hash des Session-Tokens sowie Warenkorb- oder Bestell-ID. Nie der Token selbst, keine E-Mail, keine IP-Adresse.
Widerruf
DELETE /api/shop/session/:token macht die Session sofort ungültig. Es gibt keinen Cache dazwischen; schon der nächste Request mit diesem Token erhält 401. Rufe es beim Logout auf.

Endpunkt- & Antwort-Referenz

POST/api/shop/sessionerstellen mit Bearer-Secret
DELETE/api/shop/session/:tokenwiderrufen mit Bearer-Secret

Beide Aufrufe kommen von deinem Backend: Es authentifiziert sich mit dem Mint-Secret deines Clients (erzeugt in der ADITUS-Admin-Konsole) als Bearer-Token. Eine Session pro Login ist das normale Muster; der Browser sieht nur den entstandenen Token. Websites ohne Backend nutzen diese Endpunkte nicht: Das Element erstellt seine Browser-Session selbst über den domain-beschränkten POST /api/shop/embed-session (public-key mit cart-scope="tab").

Diesen exakten Ablauf kannst du mit deinen eigenen Zugangsdaten im Integrations-Tester ausprobieren. Erstelle eine Session, mounte den Shop und durchlaufe die Journey.

Session erstellen: Request

HeaderWertBedeutung
AuthorizationBearer <mint-secret>Das Mint-Secret deines Clients, erzeugt in der ADITUS-Admin-Konsole. Nur server-seitig. Es darf nie in einen Browser oder ein App-Bundle gelangen.
Content-Typeapplication/jsonDer Body ist ein JSON-Objekt.
Idempotency-Keystring · 8–200 visible ASCII chars · required with redeemVoucherOnMintPflicht bei redeemVoucherOnMint: true: 8–200 sichtbare ASCII-Zeichen. Ein stabiler Schlüssel für einen logischen Einlöseversuch; auf deinem Backend speichern und bei Wiederholungen mit exakt demselben Request verwenden. Ein anderer Request mit demselben Schlüssel liefert 409 idempotency_conflict. Weder Mint-Secret noch diesen Schlüssel im Browser senden.
Body-FeldTypBedeutung
sharedCartKeystring · optionalIdentifizierte, server-zu-server erstellte Sessions teilen normalerweise über die vertrauenswürdige Identität. Für anonyme gemeinsame Warenkörbe MUSS dein Backend die Session mit sharedCartKey erstellen: einer opaken, stabilen Besucherreferenz aus deiner eigenen vertrauenswürdigen Besucher-Session, keiner E-Mail. Das ist ein reines Server-Session-Feld, keine Browser-Config und kein HTML-Attribut. Akzeptiere niemals beliebige E-Mail, Identität oder sharedCartKey aus dem Browser. Eine im Browser erstellte Session allein kann keine sichere Freigabe herstellen; shared ohne vertrauenswürdige Identität liefert einen expliziten Nicht-unterstützt-Fehler, auch beim Weglassen des Scopes. Explizite 'tab'-Isolation benötigt keinen sharedCartKey; es gibt keinen stillen Fallback.
publicKeystring · PflichtDer Publishable Key, für den die Session erstellt wird. Es muss derselbe Key sein, mit dem das Micro-Frontend mountet. Jeder spätere Shop-Call wird dagegen geprüft (403 bei Abweichung). Das Bearer-Secret muss zum Client dieses Keys gehören.
emailstring · optionalDer Shop-User, als der die Session agiert. Nimm sie aus deiner authentifizierten Server-Session (Login-/SSO-Cookie oder JWT), nie aus dem Browser-Request, sonst könnte ein Besucher eine fremde Session erhalten. Fehlt das Feld, entsteht eine anonyme Session: Der Besucher kann browsen und den Warenkorb füllen, aber das Micro-Frontend blockiert den Schritt aus dem Warenkorb, bis dein onUserRequired-Callback eine nutzergebundene Session liefert (siehe Optional-User-Flow). Eine mitgeschickte, aber ungültige E-Mail bleibt ein 400.
externalUserIdstring · optionalDeine stabile Nutzerreferenz aus deiner authentifizierten Server-Session. Wird an der Session gespeichert und, wenn vorhanden, für den Identitäts-Scope genutzt; ändere sie beim Token-Refresh nicht und akzeptiere sie nicht aus beliebiger Browser-Eingabe. Sie ist selbst keine Authentifizierung.
ttlSecondsnumber · optionalLebensdauer der Session in Sekunden. Default 20 Minuten, Obergrenze 24 Stunden. Nach Ablauf liefern Shop-Calls 401 session_expired. Stelle dann einen frischen Token aus; mit refreshSession in der Config fordert das Micro-Frontend ihn automatisch an.
eventSlugstring · optionalLegt den Einstiegspunkt der Journey serverseitig fest: gesetzt startet das Micro-Frontend direkt in der Artikelauswahl dieser Veranstaltung; weggelassen mit der Veranstaltungsübersicht. Da er Teil der erstellten Session ist, kann der Browser ihn nicht manipulieren. Ungültige Zeichen liefern 400 invalid_event_slug; bei einem Slug ohne passendes Live-Event (404 event_not_found) oder einem geschlossenen bzw. nicht verkaufbaren Event (409 event_not_sellable) schlägt die Session-Erstellung bewusst fehl. Es wird keine Session erstellt und es gibt keinen Rückfall auf die Veranstaltungsübersicht.
startMode "home" Einstiegsansicht der Journey. "articles" (Standard) öffnet die Artikelauswahl; "home" öffnet die Startseite der Veranstaltung mit ihren Einstiegen (Tickets, Gutschein, freigegebene Sortimente); "voucher" öffnet zuerst die Gutschein-/Ticketcode-Maske. Die Gutschein-Maske verhält sich wie unter „Gutschein einlösen / Ticket registrieren“ beschrieben. Ohne eventSlug kommt zuerst die Veranstaltungsauswahl, danach gilt der Modus für das gewählte Event.
assortmentstring · optionalBeschränkt den Artikelstart auf ein Sortiment der festgelegten Veranstaltung (braucht eventSlug; bei startMode "voucher" ohne Wirkung). Angegeben wird die Sortiments-Id oder der exakte Name (Groß-/Kleinschreibung egal); versteckte Sortimente sind erlaubt. Ein Wert ohne Treffer zeigt im Micro-Frontend eine sichtbare Fehlermeldung statt still auf die volle Liste zurückzufallen. Max. 200 Zeichen, sonst 400 invalid_assortment.
voucherCodestring · optionalGutschein-/Ticketcode, den dein Server mit der Session übergibt (beide Startmodi). Standardmäßig löst das Embed ihn bei einer frischen Journey ein, ohne festgelegtes Event erst nach der Eventauswahl; eine Ablehnung erscheint in der Maske bzw. am ersten Warenkorb. Mit redeemVoucherOnMint: true wird er stattdessen vor einer erfolgreichen Session-Antwort serverseitig reserviert – nur für ein festgelegtes Event und einen identifizierten Nutzer. Format: 1–100 Zeichen ohne Leerzeichen oder URL-Trennzeichen, sonst 400 invalid_voucher_code.
redeemVoucherOnMintboolean · optional (default false)Nur bei serverseitiger Session-Erstellung mit voucherCode, festgelegtem eventSlug, identifizierter email und Idempotency-Key auf true setzen. Die Session-Erstellung legt einen nutzergebundenen Warenkorb an und reserviert den Gutschein tatsächlich, bevor er die Session ausgibt. Das Embed übernimmt den Warenkorb ohne zweite Anlage oder Einlösung; beide Startmodi funktionieren. Nur Gutschein-/Ticketcodes, keine PromotionCodes. Ohne die Option gilt die normale voucherCode-Übergabe. Registrierung und Abschluss folgen weiterhin.

Session erstellen: Antwort (200)

FeldTypBedeutung
sessionTokenstringOpaker Token (sess_…). An den Browser weiterreichen und bei mount() als sessionToken setzen; das Micro-Frontend sendet ihn bei jedem Shop-Call als X-Aditus-Session. Er enthält keine Nutzerdaten und ist nicht dekodierbar.
expiresAtnumberAblauf als Unix-Timestamp in Millisekunden. Rein informativ für deine eigene Planung. Das Micro-Frontend reagiert selbst auf das 401.
voucherRedeemedboolean · only with redeemVoucherOnMinttrue, wenn die optionale Session-Erstellung mit vorbereitetem Gutschein erfolgreich ist. Der zurückgegebene Session-Token besitzt bereits einen Warenkorb mit Gutscheinreservierung; das Embed übernimmt ihn. Dies ist keine abgeschlossene Ticketregistrierung.

Session widerrufen

DELETE /api/shop/session/:token mit demselben Bearer-Secret. Es muss zum Client gehören, für den die Session erstellt wurde. Beim Logout oder Kiosk-Reset aufrufen, damit der Token zusammen mit deiner eigenen Session stirbt. Bei anonymen (token-gebundenen) Sessions löscht der Aufruf zusätzlich die Warenkörbe der Session in der Shop-API. Von diesen Warenkörben gehaltene Gutschein-Reservierungen werden sofort frei, nicht erst mit dem Ablauf des Warenkorbs. Antwort: { "revoked": true, "carts": { "scope": "token", "deleted": 1, "failed": 0 } } (revoked false, wenn die Session schon abgelaufen war; scope "identity" bei nutzergebundenen Sessions, deren Warenkörbe einen einzelnen Token überdauern und unangetastet bleiben). Der Widerruf ist idempotent und kann fire-and-forget gesendet werden.

Server-zu-Server-Reads

Ein Backend, das das Micro-Frontend steuert (eine Kiosk-Shell, ein Onsite-Shop) braucht meist Mandantenfakten, bevor es eine Session erstellt: welche Events es gibt und im Verkauf sind, welche Sprachen der Shop spricht, welche Lookup-Listen und welches Branding gelten. Vier Lese-Endpunkte beantworten das mit denselben Zugangsdaten wie die Session-Erstellung (Bearer Mint-Secret + Header X-Aditus-Public-Key, gleiches Rate-Limit, Cache-Control: no-store). Sie sind Whitelist-Projektionen der Shop-API, nie eine Rohweiterleitung; ?language= (oder Accept-Language, Standard de) wählt die Sprache der Namen. Fehler: 400 invalid_public_key, 401 unauthorized, 503 shop_api_not_configured, 502 upstream_error.

EndpunktLiefert
GET /api/shop/events?language=deAlle Events des Mandanten: id, slug, name, year, start/end, location, saleState (open · registration_only · closed), die Rohflags, showInOverview, languages, Icon-/Logo-URLs. Den Slug für die Session nimmst du von hier.
GET /api/shop/configurationMandantendaten: mandator, die Namen der Lookup-Listen (Anreden, Länder, Titel, Cookie-Consent) und websiteMetricTracking. Login-Provider-Zugangsdaten des Mandanten sind bewusst nie enthalten.
GET /api/shop/languagessystemLanguage plus die Liste der Shop-Sprachen ({ code, name }). Dies sind die Werte, die das culture-Attribut des Micro-Frontends akzeptiert.
GET /api/shop/branding?images=ONSITE_SHOP_LOGO,…Das Shop-Theme des Mandanten: styles als flache key → value-Map, die Theme-Bilder mit URLs und (für bis zu 40 von dir genannte Bild-Keys (A–Z, 0–9, _ , -)) ob der Key existiert, seine öffentliche URL und der Content-Type. Bild-URLs brauchen keine Auth und lassen sich direkt in deiner Shell verwenden. Mit &eventSlug=<slug> oder &event=<GUID> beziehst du Theme und Keys auf ein Event (Event-Overrides, nur dort vorhandene Keys wie ONSITE_SHOP_HEADER / ONSITE_SHOP_ATTRACT_*); die Antwort nennt dann das aufgelöste Event. Unbekannter Slug → 404 event_not_found, kein GUID → 400 invalid_event, Shop-API nicht erreichbar → 503 event_unavailable.

Fehler sind explizit

StatusCodeBedeutung
503session_not_configuredMinting ist für diesen Client deaktiviert: Im Admin wurde noch kein Mint-Secret erzeugt.
401unauthorizeddas Mint-Secret ist falsch oder fehlt. Prüfe, ob das Secret zum Client genau dieses Public Keys gehört (ein rotiertes Secret macht das alte sofort ungültig).
400invalid_public_keyder Public Key ist fehlerhaft oder nicht registriert. Er muss die Form pk_… haben und zu einem eingerichteten Client gehören.
400invalid_emaildie E-Mail des Shop-Users fehlt oder ist keine gültige Adresse.
400invalid_tokender Session-Token im Revoke-Aufruf ist fehlerhaft. Übergib genau den sessionToken aus der Mint-Antwort.
429rate_limitedzu viele Mint-/Revoke-Aufrufe von deiner IP. Warte eine Minute und versuch es erneut.
403public_key_invalidder Header X-Aditus-Public-Key ist fehlerhaft. Er muss die Form pk_… haben, genau wie beim Onboarding ausgegeben.
403public_key_unknownder X-Aditus-Public-Key ist wohlgeformt, aber nicht registriert (oder deaktiviert). Prüfe auf Tippfehler und ob der Client des Keys aktiv ist.
403public_key_origin_unresolveddie Anfrage trug einen publicKey, aber keinen verwertbaren Origin-/Referer-Header, daher kann die Domain-Whitelist nicht geprüft werden. Browser senden Origin automatisch; Server-zu-Server-Aufrufe dürfen den publicKey-Header nicht nutzen.
403public_key_domain_not_alloweddie Herkunftsdomain der Anfrage steht nicht auf der Whitelist dieses publicKey. Lass die Domain beim Onboarding (oder im Admin) freischalten, bevor du dort live gehst.
401invalid_sessionder Shop-Call trug einen fehlerhaften X-Aditus-Session-Token. Es gibt keinen Rückfall auf einen anderen Nutzer. Übergib genau den sessionToken (sess_…) aus der Mint-Antwort.
401session_expiredder X-Aditus-Session-Token ist unbekannt, widerrufen oder abgelaufen. Es gibt keinen Rückfall auf einen anderen Nutzer. Minte server-zu-server eine frische Session.
403session_requires_public_keyeine Session wurde ohne den Header X-Aditus-Public-Key gesendet (Origin-Pinning ist Pflicht, sobald eine Session im Spiel ist).
403session_key_mismatchder X-Aditus-Public-Key passt nicht zu dem Key, für den die Session gemintet wurde.
403anonymous_sessiondie Session wurde OHNE E-Mail gemintet (anonym) und erlaubt nur Browsen und Warenkorb. Registrierung, Zahlung und Checkout brauchen eine nutzergebundene Session. Erstelle sie über onUserRequired.

Jetzt zu deinem Look

Die Identität steht. Jetzt branden wir das Micro-Frontend. Das Styling-Tool erzeugt eine fertige Theme-Config zum Einfügen und erklärt jeden Parameter, mit Live-Vorschau.

Zum Styling-Tool

Du arbeitest mit einem Coding-Agenten? Übergib ihm den maschinenlesbaren Integrationsleitfaden für das Micro-Frontend. Kopiere die kanonische URI unten und füge sie bei deinem Agenten ein.

https://developers.aditus.com/ai/micro-frontend.md