# Embed the Creative Simulator via `<iframe>`

> How to embed the Creative Simulator in your website via iframe and drive it through postMessage with your own products and creatives. Live demo and code included.
>
> Human-readable version: https://virtualcampaign.dev/en/embed (German: https://virtualcampaign.dev/embed)
> Live sandbox: https://demo.kreativsimulator.com/
> This file is auto-generated from the live docs on every build — it is always current.

A single iframe tag puts the Creative Simulator on your own website. If you like, you can also drive it from the outside with your own product picker, creatives and controls. This guide shows every step with code you can copy. Further down you can try it all in a live demo.

## Step 1: Whitelist your domains

For security reasons the browser only allows embedding **from whitelisted domains** (`Content-Security-Policy: frame-ancestors`).

**Every domain** the iframe should appear on must be whitelisted. Wildcards are supported.

For example:

- `https://www.your-agency.com`
- `https://campaign.your-agency.com`
- `https://*.your-agency.com  (wildcard)`

### Who enters the domains?

- **Tenants with access to VirtualCampaign** maintain the list themselves: *Settings → Kreativsimulator → Websites allowed to embed*, one address per line. Title, language, logo and theme are set there too. Changes take effect within one minute.
- **Everyone else** sends us the domains and we enter them.

> **URL parameters of your embed** (`?theme=`, `?lang=`, `?logo=` …) **always take precedence** over the settings in VirtualCampaign.

**Important:** Without whitelisting the iframe stays blank (white area). The same goes for the live demo below: it only loads on a whitelisted domain. Otherwise use the “Open in new tab” link.

## Step 2: Embed code

A single `<iframe>` tag is enough. You need no API key in the browser and no further configuration, because authentication runs entirely server-side. Only if you protect access with a token does your server fetch one token per page view (see step 9).

```html
<iframe
  src="https://demo.kreativsimulator.com/"
  title="Kreativsimulator"
  style="width: 100%; height: 720px; border: 0;"
  allow="fullscreen"
  loading="lazy"
></iframe>
```

**Embed URL:** `https://<your-tenant>.kreativsimulator.com/`

The tenant is derived from the subdomain. For the public sandbox use `demo.kreativsimulator.com`.

You can preset the language and start product with query parameters. The next section explains how.

## Step 3: URL parameters

Optionally append these query parameters to the embed URL (start with `?`, chain with `&`):

| Parameter | Values | Effect | Default |
| --- | --- | --- | --- |
| `lang` | `de · en · fr · it` | UI language | Tenant → browser → de |
| `product` | Product **code** (`ec5472c2`) | Loads this product directly (deep link). Unknown code → first product. | First product |
| `header` | `0 · 1` | Show/hide the header (product selector) | `1` (visible) |
| `sidebar` | `0 · 1` | Show/hide the sidebar. It opens with a click or tap on a placeholder in the image or with the tab on the right edge. A tap on a still empty placeholder opens the creative picker right away. Without the sidebar the click opens the file picker. | `1` (visible) |
| `playback` | `0 · 1` | Show/hide the playback/transport buttons in the bar | `1` (visible) |
| `timeline` | `0 · 1` | Show/hide the timeline (scrubbing bar) | `1` (visible) |
| `export` | `0 · 1` | Show/hide the export button on the right of the playback bar: with the selection enabled it shows the share icon (tooltip “Export and share”), otherwise the download icon (tooltip “Export creative”). Without it the dialog keeps only the “My selection” tab. | `1` (visible) |
| `effects` | `0 · 1` | Show/hide the effect picker (photo products only, button bottom left on the image), e.g. for your own picker | `1` (visible) |
| `view` | View key (`0e23675447`) | **Only with the view picker enabled** (see “Views”): starts the product from `product` in this view. Unknown key → default view. | Default view |
| `views` | `0 · 1` | **Only with the view picker enabled:** show/hide the “Views (n)” button top left on the image, e.g. for your own picker | `1` (visible) |
| `selection` | `0 · 1` | **Only with the selection enabled** (see “Selection, share link and slideshow”): show/hide the ＋ “Add” and “Selection” buttons | `1` (visible) |
| `library` | `0 · 1` | **Only with the product library enabled** (see “Product library”): show/hide the “Catalog (n)” button. The product bar stays. | `1` (visible) |
| `hint` | `0 · 1` | Tap hint: after the first load the placeholders pulse once in the accent color (`--vc-hint-color`), so users see they can tap them. Once a user has tapped a placeholder, it does not come back. Never in the export. `?embed=kiosk` switches it off. | `1` (on) |
| `editor` | `auto · sidebar · overlay` | Where placeholders are edited. `overlay` = a tap on the placeholder opens the editor right on the image (effect icons and sliders, every change visible at once, ✓ apply, ✕ discard) instead of the sidebar. `sidebar` = always the sidebar. `auto` = the editor on the image on phones (up to 768 px), the sidebar on desktop. With `sidebar=0` or `embed=kiosk` the editor on the image never opens by itself. | `auto` |
| `sel` | value from `selectionLinkCopied.param` or `selectionChanged.param` | **Only with the selection enabled:** opens a shared or saved selection (products, views and creatives by URL) | – |
| `fullscreen` | `0 · 1` | Fullscreen icon: `0`=never, `1`=always (incl. desktop); omitted = mobile / in fullscreen only | adaptive |
| `fit` | `0 · 1` | `1` = the iframe gets the layout of the simulator's own page: the canvas fits width **and** height and sits centred, the open sidebar takes room on the right instead of covering the canvas, and the button bar wraps when space is short. Recommended for every embed with a visible sidebar. | `0` (existing iframe layout) |
| `embed` | `minimal · kiosk` | Shorthand: hides header, sidebar and export (playback bar stays) | – |
| `theme` | URL to a CSS file | Loads a custom theme (CSS variables) that overrides the tenant theme | – |
| `bg` | **any CSS color**, `transparent` **or** image URL | Background of the area **around** the canvas (letterbox/frame), not the video | Theme background |
| `stage` | `auto` · `<width>x<height>` (`1024x1024`) | **Shape and size of the canvas.** `auto` = film/clip 16:9, **photo in its own aspect ratio** (whole photo, fluid). `WxH` = fixed shape **and** fixed display pixel size for every product (the image is cover-fitted, edges are cropped). Changeable at runtime via `setStage`/`setProduct{stage}`. | `auto` |
| `placeholderBg` | any CSS color | Background of the **placeholder box** (loader without a creative) – direct shortcut, no `?theme=` file | Theme/default (`#ffffff`) |
| `placeholderText` | any CSS color | Color of the letter inside the placeholder box | Theme/default (`#000000`) |
| `placeholderBorder` | any CSS color | Border color of the placeholder box | Theme/default (`#000000`) |
| `watermark` | `0 · 1` | `1` = every exported MP4 **and** every saved still image (JPG) gets the “Vorschau” (preview) watermark (dialog **and** postMessage export). **Additive only** – on the demo tenant it is always on and cannot be disabled. | `0` (demo tenant only) |
| `logo` | `0 · 1` | `0` = exported MP4 **and** still image **without** the tenant logo top-left (dialog **and** postMessage export). Overridable per export with the `logo` field on `export`/`exportImage`. | `1` (logo in the export), unless the tenant has “no logo” as its default (ask us) |

- **No custom branding inside the UI:** the simulator shows **no logo and no title**. The header contains only the product selector (and the sidebar tab); branding comes from your page around the iframe. **The exported MP4 and the saved still image carry the tenant's logo** composited top-left by default. On request we switch your tenant's default to “no logo”. You also control it yourself: `?logo=0` switches it off for every export of this embed, the field `logo: false`/`true` on `export`/`exportImage` for a single export.
- **Boolean parameters** accept `0`/`1` (also `true`/`false`, `yes`/`no`). `embed=kiosk` is evaluated first – individual parameters override it afterwards (e.g. `?embed=kiosk&header=1` still shows the header).
- **The visibility switches** (`header`/`sidebar`/`playback`/`timeline`/`export`/`effects`/`hint`/`editor`, when enabled also `views`/`selection`/`library`) can also be toggled **at runtime** via `setChrome` (see API). Playback, export and effect selection always work via postMessage, even when the matching controls are hidden. So you can build a fully custom UI (your own effect picker via `setEffect`, hide the built-in effect picker with `effects=0`).
- **Tap hint and editor apply to existing embeds too:** the tap hint is on by default, switch it off with `?hint=0` or `setChrome {hint: false}`. On phones (up to 768 px) a tap on a placeholder opens the editor right on the image. `?editor=sidebar` shows the sidebar everywhere. The protocol stays `v: 1`, existing messages get no new fields.
- **theme CSS:** loaded as an extra `<link>` **after** the tenant theme, overriding its CSS variables (e.g. `--vc-accent`, `--vc-bg`). CSS cannot run JavaScript; the file may be cross-origin (**no CORS needed**). Values for `?theme=`/`?bg=` must be URL-encoded (e.g. `%23` for `#`).
- **`bg=transparent`:** the **parent page shows through** the area around the canvas (best with `?embed=kiosk`). The video canvas itself always stays opaque – transparent only affects the frame/letterbox edges.
- **Placeholder colors directly in the URL** (`placeholderBg`/`placeholderText`/`placeholderBorder`): colors just the **placeholder box** (the generated letter motif shown until a creative is set). It is the **short route with no `?theme=` file**. Accepts any CSS color (hex `%23ff5a10`, `rgb(...)`, `hsl(...)`, names like `white`) and overrides the tenant theme **and** `?theme=`; only the params you set override. *Tip:* don't set `placeholderBg` equal to `placeholderText` (invisible letter). For more branding (accent, buttons, spinner …) use `?theme=`.

### Examples

```text
https://demo.kreativsimulator.com/                          # default
https://demo.kreativsimulator.com/?lang=en                  # English UI
https://demo.kreativsimulator.com/?product=ec5472c2         # open a product
https://demo.kreativsimulator.com/?product=ec5472c2&lang=fr # language + product
https://demo.kreativsimulator.com/?embed=kiosk              # canvas + playback bar only
https://demo.kreativsimulator.com/?header=0&sidebar=0       # header + sidebar off
https://demo.kreativsimulator.com/?timeline=0&playback=0    # timeline + transport off
https://demo.kreativsimulator.com/?effects=0                # effect picker off (own picker)
https://demo.kreativsimulator.com/?product=ec5472c2&view=0e23675447  # photo product in this view (views enabled only)
https://demo.kreativsimulator.com/?views=0                  # "Views (n)" button off (own picker, views enabled only)
https://demo.kreativsimulator.com/?selection=0              # selection buttons off (selection enabled only)
https://demo.kreativsimulator.com/?library=0                # "Catalog (n)" button off (product library enabled only)
https://demo.kreativsimulator.com/?hint=0                   # no tap hint (placeholders do not pulse)
https://demo.kreativsimulator.com/?editor=overlay           # edit placeholders in the editor on the image
https://demo.kreativsimulator.com/?editor=sidebar           # always the sidebar, also on phones
https://demo.kreativsimulator.com/?sel=1.eyJ…               # open a shared or saved selection (selection enabled only)
https://demo.kreativsimulator.com/?fullscreen=1             # fullscreen icon also on desktop
https://demo.kreativsimulator.com/?fit=1                    # own-window layout in the iframe (recommended with sidebar)
https://demo.kreativsimulator.com/?watermark=1              # every export gets the preview watermark
https://demo.kreativsimulator.com/?logo=0                   # exports without the tenant logo
https://demo.kreativsimulator.com/?stage=1280x720           # fixed 1280×720 canvas (16:9)
https://demo.kreativsimulator.com/?stage=1024x1024          # square canvas, 1024 px
https://demo.kreativsimulator.com/?bg=%23000000             # black letterbox area
https://demo.kreativsimulator.com/?embed=kiosk&bg=transparent  # parent page shows through
https://demo.kreativsimulator.com/?theme=https%3A%2F%2Fcdn.kunde.de%2Fks-theme.css

# Placeholder colors directly (no theme file) — encode # as %23:
https://demo.kreativsimulator.com/?placeholderBg=%23ff5a10&placeholderText=%23ffffff&placeholderBorder=%23ffffff
# … or with rgb()/color names (no %23 needed):
https://demo.kreativsimulator.com/?placeholderBg=rgb(255,90,16)&placeholderText=white&placeholderBorder=white
```

## Step 4: Embed responsively

In **embed mode** (chrome off, e.g. `?embed=kiosk` or `?sidebar=0`) the canvas **fills the iframe edge-to-edge** and stays **centered**. So just drop the iframe with `width:100%; height:100%` into any sized parent container (flexbox/grid too). If its aspect ratio differs from the canvas's you get a **symmetric** letterbox (color via `?bg=`).

We recommend an iframe that fills its flex or grid container:

```html
<!-- Any parent container; the iframe fills it completely. -->
<div style="display:flex; width:100%; height:480px;">
  <iframe
    src="https://demo.kreativsimulator.com/?embed=kiosk"
    title="Kreativsimulator"
    style="flex:1; border:0;"
    allow="fullscreen"
    loading="lazy"
  ></iframe>
</div>
```

For an exact aspect ratio without letterboxing, give the wrapper `aspect-ratio: 16 / 9` – that fits film and clip products:

```html
<!-- Exact 16:9, no letterbox: constrain the wrapper. -->
<div style="aspect-ratio: 16 / 9; width: 100%;">
  <iframe
    src="https://demo.kreativsimulator.com/?embed=kiosk"
    title="Kreativsimulator"
    style="width: 100%; height: 100%; border: 0;"
    allow="fullscreen"
    loading="lazy"
  ></iframe>
</div>
```

### What shape is the canvas? The stage

The shape is set by the **stage** (`?stage=`, default `auto`): film and clip products are **16:9**, photo products use the **aspect ratio of the photo** (e.g. 3:2 or portrait) – the whole photo is visible, nothing is cropped. The simulator reports the current shape in the `render` block of `ready` and `productChanged`, and in the `stageChanged` event (`width`, `height`, `aspectRatio`). Size your iframe box from it and no letterbox is left:

```js
// Let the iframe box follow the canvas shape — no letterbox, no matter
// whether a 16:9 film or a portrait photo is loaded.
window.addEventListener("message", (e) => {
  const m = e.data;
  if (!m || m.source !== "kreativsimulator") return;
  // ready / productChanged carry `render`; stageChanged carries the fields itself.
  const r = m.type === "stageChanged" ? m : m.render;
  if (r) iframe.style.aspectRatio = r.width + " / " + r.height;
});

// Force ONE fixed shape for every product instead (e.g. square tiles):
// send({ type: "kreativsimulator:setStage", stage: "1024x1024" });
// send({ type: "kreativsimulator:setStage", stage: "auto" });   // back to per-product
```

> If you want **one fixed shape for every product** (e.g. square tiles), set `?stage=1024x1024` or send `setStage`. The image is then cover-fitted into that shape: a 16:9 film in a square loses about 22% on the left and on the right – placeholders near the edge can fall out of the picture. Check this per product.

With the sidebar visible (without `?embed`/`?sidebar=0`) the canvas stays left-aligned **in the iframe** (max 1280px). The fill and centre logic applies there only in embed mode, so existing embeds keep their look. When the simulator is opened directly as its own page (e.g. from a link), the canvas fits width **and** window height, and the button bar wraps cleanly when space is short. **`?fit=1` gives the iframe the same layout.** It is the recommended setting for embeds with a visible sidebar: without `fit` the sidebar can cover the canvas and the playback bar can slip below the edge when the iframe is not tall enough. **Button bar:** one row, centred as a whole under the image, on phones as on desktop. It holds playback (stop, ◀, ▶, ▶|), “Add” and export. All buttons are 32 px high, icons have a tooltip and an `aria-label`. “Add” exists only with the selection enabled. The text button uses the main color in the “Products” tab and keeps its width when tapped. The export button shows the share icon with the selection enabled, otherwise the download icon. With the selection, the “My selection” tab has “Play whole selection” with the music button below. The same height stays free in the “Products” tab, so nothing jumps when you switch tabs.

### Controls inside the iframe

- **Buttons on the image:** “Views (n)” top left (view picker only), the effect picker of photo products bottom left (it opens a dialog with the list), fullscreen top right and “1:1” below it once you have zoomed. This keeps the bar under the image low and makes the canvas in the iframe larger. Hide them with `?views=0` or `?effects=0`.
- **Header with the product library:** “Catalog (n)”, “Products (n)” and “My selection” are text labels. On phones (up to 768 px) a toggle bar “Products (n) | My selection” spans the full width at the top, with a round button in the theme color next to it that opens the sidebar. A fixed “Catalog” tile (magnifier and word, active filters as a number) sits left of the product bar, and in the “My selection” tab a “Clear” tile (bin and word, with a confirmation) takes its place. Both are as tall as the product tiles. Products already in the selection carry a small ✓. The bar scrolls smoothly and keeps the active product in the middle, without a scrollbar on touch devices. An empty selection shows a notice that points to “Add”. The margin around image and header is 8 px.
- **Editor on the image:** the default on phones, everywhere with `?editor=overlay`, never with `?editor=sidebar`. The editor covers the whole iframe and works like a photo editor. The image fills the area, zoomed onto the placeholder. Top left is “Creative” (with several surfaces: this surface only or all of them), top right ✕ (discard) and ✓ (apply). The functions sit at the bottom as icons, with the sliders of the chosen function above them as a collapsible strip (icon, slider, ✓ done, on/off and reset per function). An “OK” button above the sliders closes them. A tap on an empty surface opens the creative picker, and the editor then switches to “Transform”. A filled surface swaps its creative via “Creative”. A tap on another surface switches to it. The ⋯ menu offers reset effects, creative on all surfaces, apply effects to all surfaces and reset everything. Bottom left, “1:1” shows the whole image and “Fit” centres the surface. Two fingers or the mouse wheel zoom, a double tap jumps to “Fit”. Changes apply to every scene of the placeholder.
- **Fullscreen:** fullscreen has no bar. A round play/pause button sits bottom right instead, with the effect picker on the left at the same height. Both keep a gap to the bottom edge so the phone's own hints do not cover them.
- **Choose a creative:** the dialog shows the current creative, with in/out for videos, and has a field for a link to an image, video or stream. On phones it uses the full height.
- **Zoom in the image:** two fingers zoom into the image, on desktop Ctrl and mouse wheel or the trackpad. The “1:1” button resets it. Vertical one-finger scrolling still moves your page.

## Step 5 — The most common case: Just the canvas, with controls you build yourself

Usually you show **only the WebGL canvas** and build the entire UI yourself. To do that, hide header, sidebar and buttons via URL with `?embed=kiosk` (header + sidebar + export off, playback bar stays) or selectively with `?header=0&sidebar=0&playback=0&export=0`. At runtime the same works via `setChrome`.

**Playback and export always work via postMessage**, even when the built-in buttons are hidden. So your own buttons drive the iframe. For a custom progress bar, listen to the `playbackState` event.

When the sidebar is hidden, clicking a placeholder directly on the canvas opens the file dialog for exactly that placeholder. Your users can upload a creative even without an upload field of your own.

`embed=kiosk` hides the built-in chrome – your buttons send `play`/`pause`/`seek`/`export`. Try it in the live demo below with the “Chrome” toggles and the custom playback bar.

Canvas in kiosk mode + your own playback controls, kept in sync via the `playbackState` event:

```html
<!-- Canvas only — your own playback UI drives it from the outside -->
<iframe id="ks" src="https://demo.kreativsimulator.com/?embed=kiosk"
        style="width:100%;height:720px;border:0" allow="fullscreen"></iframe>

<button id="play">▶︎</button>
<button id="pause">⏸</button>
<button id="stop">⏹</button>
<button id="back">⏮</button>
<button id="fwd">⏭</button>
<input id="scrub" type="range" min="0" value="0" />
<button id="export">Export</button>
<button id="still">Save image</button>
<button id="fs">⛶ Fullscreen</button>

<script>
  const iframe = document.getElementById("ks");
  const send = (msg) => iframe.contentWindow.postMessage(msg, "*");
  const P = "kreativsimulator:";

  play.onclick  = () => send({ type: P + "play" });
  pause.onclick = () => send({ type: P + "pause" });
  stop.onclick  = () => send({ type: P + "stop" });
  back.onclick  = () => send({ type: P + "stepBackward" });
  fwd.onclick   = () => send({ type: P + "stepForward" });
  scrub.oninput = (e) => send({ type: P + "seek", frame: Number(e.target.value) });
  // Direct export at 1280×720 (no dialog):
  export.onclick = () =>
    send({ type: P + "export", filename: "creation", width: 1280, height: 720 });
  // Still image (JPG) of the currently shown frame (photo products: the whole photo):
  still.onclick = () =>
    send({ type: P + "exportImage", filename: "creation", width: 1280, height: 720 });
  // Fullscreen ON reliably via requestFullscreen on the iframe (the user gesture
  // is here, on the click). OFF/toggle could also go via the postMessage event.
  fs.onclick = () => {
    if (document.fullscreenElement) document.exitFullscreen();
    else iframe.requestFullscreen();   // iframe needs allow="fullscreen"
  };

  window.addEventListener("message", (e) => {
    const m = e.data;
    if (!m || m.source !== "kreativsimulator") return;
    if (m.type === "playbackState") {
      scrub.max = m.frameCount - 1;
      if (document.activeElement !== scrub) scrub.value = m.frame; // don't fight the drag
      play.disabled = m.playing;
      pause.disabled = !m.playing;
    }
    if (m.type === "fullscreenChanged") {
      fs.textContent = m.fullscreen ? "⛶ Exit fullscreen" : "⛶ Fullscreen";
    }
  });

  // Ask for the current state once (e.g. after load):
  // send({ type: P + "getState" });
</script>
```

## Step 6 — Custom design: Your own design via CSS (theme & bg)

The iframe adopts your branding through CSS variables. `?theme=` loads a custom CSS file **after** the tenant theme and overrides its variables (e.g. `--vc-accent`, `--vc-bg`). `?bg=` sets the letterbox area **around** the canvas (color or image URL), `?stage=` the shape and size of the canvas.

### Embed with theme + background

```html
<!-- Your own theme + a black letterbox around the canvas -->
<iframe
  src="https://demo.kreativsimulator.com/?embed=kiosk&theme=https%3A%2F%2Fcdn.kunde.de%2Fks-theme.css&bg=%23000000"
  title="Kreativsimulator"
  style="width: 100%; height: 720px; border: 0;"
  allow="fullscreen"
  loading="lazy"
></iframe>
```

### Theme template (all overridable CSS variables)

```css
/* =============================================================================
   Kreativsimulator – Theme-Vorlage (CSS Custom Properties)
   =============================================================================

   So passt du das Aussehen des eingebetteten Kreativsimulators an deine Marke an.

   VERWENDUNG
   ----------
   1. Diese Datei kopieren, Werte unten anpassen, auf einem öffentlich
      erreichbaren HTTPS-URL ablegen (z. B. https://cdn.deine-agentur.de/ks-theme.css).
   2. Den URL per `?theme=` an die iframe-Einbettung hängen – URL-kodiert:

      <iframe src="https://demo.kreativsimulator.com/?theme=https%3A%2F%2Fcdn.deine-agentur.de%2Fks-theme.css"
              style="width:100%;height:720px;border:0" allow="fullscreen"></iframe>

   HINWEISE
   --------
   • Die Datei wird als zusätzliches <link> NACH dem Mandanten-Theme geladen und
     überschreibt dessen Werte (CSS-Kaskade). Cross-Origin ist erlaubt (kein CORS
     nötig) – CSS kann kein JavaScript ausführen.
   • Du musst NICHT alle Variablen setzen. Lösche einfach die Zeilen, die du nicht
     ändern willst – dann gilt der jeweilige Standard des Simulators/Mandanten.
   • Farben: jede gültige CSS-Farbe (#hex, rgb(), hsl(), benannt).
   • Den Hintergrund UM den Canvas (Letterbox) steuerst du besser per
     `?bg=<farbe|transparent|url>` – nicht über `--vc-bg` (das ist auch der
     Canvas-Hintergrund während des Ladens).
   ========================================================================== */

:root {
  /* ── Meta ──────────────────────────────────────────────────────────────
     Hell-/Dunkel-Schema. Beeinflusst u. a. native Formular-Elemente.
     Werte: dark | light */
  --vc-color-scheme: dark;

  /* Schriftfamilie der gesamten Oberfläche.
     Eigene Webfont? Oben in der Datei per @import laden, z. B.:
       @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap');
     und hier referenzieren: 'Inter', system-ui, sans-serif */
  --vc-font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;

  /* ── Flächen ───────────────────────────────────────────────────────────
     Grundfarben der UI-Container (Sidebar, Dialoge, Karten). */
  --vc-bg: #0a0a0a;             /* App-Hintergrund / Canvas-Hintergrund beim Laden */
  --vc-surface: #181818;       /* Sidebar, Dialog-Flächen */
  --vc-surface-raised: #2a2a2a;/* erhöhte Elemente (Tracks, Chips, Inputs) */
  --vc-surface-hover: #3a3a3a; /* Hover-Fläche */
  --vc-border: #2a2a2a;        /* Standard-Rahmenfarbe */
  --vc-border-strong: #444444; /* betonte Rahmen */

  /* ── Text ──────────────────────────────────────────────────────────────*/
  --vc-text: #eeeeee;          /* Standard-Textfarbe */
  --vc-text-muted: #888888;    /* sekundärer Text */
  --vc-text-subtle: #666666;   /* dezente Hinweise */
  --vc-text-on-accent: #ffffff;/* Text auf Akzent-/Primärflächen */

  /* ── Akzent (Marke) ────────────────────────────────────────────────────
     Wird u. a. für aktive Zustände, Timeline/Abspielkopf und Slider-Punkte
     verwendet. `--vc-accent-rgb` MUSS dieselbe Farbe als "R, G, B" enthalten
     (ohne rgb()-Klammern) – wird für halbtransparente Hervorhebungen genutzt. */
  --vc-accent: #df3b00;
  --vc-accent-strong: #ff5722; /* reserviert — wird derzeit von keinem UI-Element gelesen */
  --vc-accent-rgb: 223, 59, 0; /* = --vc-accent als R, G, B */
  --vc-accent-muted: #3a3a3a;  /* gedämpfter Akzent-Hintergrund */
  --vc-danger: #e5484d;        /* Löschen (Mülleimer in der Auswahl-Liste, „Auswahl leeren") */
  --vc-warning: #f5a524;       /* Warnung: leere Werbeflächen vor Export/Teilen */
  --vc-hint-color: #df3b00;    /* Tipp-Hinweis: Platzhalter pulsieren einmal (Default = --vc-accent) */

  /* ── Header (Kopfzeile mit Produktauswahl) ─────────────────────────────
     Nur relevant, wenn der Header sichtbar ist (sonst per ?header=0 ausblenden). */
  --vc-header-bg: transparent;     /* Hintergrund der Kopfzeile */
  --vc-header-text: #eeeeee;       /* Header-Text (aktives Produkt-Label, Menü-Icon) */
  --vc-header-text-muted: #888888; /* sekundärer Header-Text (inaktive Produkt-Labels) */
  --vc-header-border: #1a1a1a;     /* untere Trennlinie */
  --vc-header-border-width: 1px;   /* Stärke der Trennlinie (0px = keine) */
  --vc-header-padding: 12px 20px;  /* Innenabstand der Kopfzeile */

  /* ── Buttons ───────────────────────────────────────────────────────────*/
  --vc-button-bg: #2a2a2a;       /* Button-Fläche */
  --vc-button-bg-hover: #3a3a3a; /* Button-Fläche bei Hover */
  --vc-button-text: inherit;     /* Button-Textfarbe */
  --vc-button-border: #444444;   /* Button-Rahmen */
  --vc-button-radius: 4px;       /* Eckenradius der Buttons */
  /* Inaktiver Auswahl-Button (Platzhalter-/Szenen-Selector). Bewusst NICHT die
     Primärbutton-/Akzentfarbe — neutraler Ton, damit aktiv/inaktiv sichtbar trennt. */
  --vc-button-inactive-bg: #3a3a3a;   /* Fläche des nicht-aktiven Selector-Buttons */
  --vc-button-inactive-text: #eeeeee; /* dessen Textfarbe (auf der Fläche lesbar) */

  /* ── Slider / Zeitleiste ───────────────────────────────────────────────
     Die In/Out-Marken nutzen --vc-accent (s. o.); der Abspielkopf lässt sich
     darüber hinaus separat einfärben. */
  --vc-slider-track: #3a3a3a;    /* Track-Farbe der Regler */
  /* Abspielkopf (Linie + Griff) BEIDER Zeitleisten — Haupttimeline und
     Platzhalter-Mini-Leiste. Ohne diese Zeile folgt er --vc-accent. */
  --vc-playhead-color: #df3b00;
  /* Bereich außerhalb von In/Out in beiden Zeitleisten. Bei einem HELLEN Theme
     unbedingt beide Werte anpassen (s. Beispielblock unten) — sonst wirkt der
     inaktive Bereich dunkler als der aktive und die Timecodes verschwinden. */
  --vc-timeline-dim: rgba(0, 0, 0, 0.55);      /* Schleier außerhalb In/Out */
  --vc-timeline-dim-text: #ffffff;             /* Timecodes im Schleier */

  /* ── Vorschau-Canvas (Rahmen um das Video) ─────────────────────────────*/
  --vc-canvas-outline-color: #2a2a2a; /* Outline-Farbe des Canvas */
  --vc-canvas-outline-width: 1px;     /* Outline-Stärke (0px = kein Rahmen) */
  --vc-canvas-radius: 0px;            /* Abgerundete Canvas-Ecken (0px = eckig) */

  /* ── Lade-Spinner (während Hintergrund/Produkt lädt) ───────────────────
     Ohne diese Zeilen folgt der Spinner --vc-accent (Bogen) / --vc-border (Ring). */
  --vc-spinner-color: #df3b00; /* rotierender Bogen (Default: Akzent) */
  --vc-spinner-track: #2a2a2a; /* Ring/Spur (Default: Border) */
  --vc-spinner-size: 32px;     /* Durchmesser */

  /* ── Viewport-Rahmen (optionaler Rahmen um die ganze Fläche) ───────────*/
  --vc-frame-color: transparent; /* Farbe des Außenrahmens */
  --vc-frame-width: 0px;          /* Stärke (0px = aus) */

  /* ── Platzhalter (Plakatflächen ohne hochgeladenes Motiv) ──────────────
     Die generierte „A/B"-Platzhaltertextur. */
  --vc-placeholder-bg: #ffffff;     /* Hintergrund der Platzhalterfläche */
  --vc-placeholder-text: #000000;   /* Buchstabe/Beschriftung */
  --vc-placeholder-border: #000000; /* Rahmen der Platzhalterfläche */

  /* ── Dialoge & Eingabefelder ───────────────────────────────────────────
     Export-Dialog („Export als MP4") sowie alle Input-/Select-Felder
     (Dateiname, Auflösung, Effekt-Dropdown). Ohne diese Zeilen folgt alles
     den Surface-/Text-Defaults oben. */
  --vc-dialog-bg: #181818;                /* Dialog-Fläche */
  /* --vc-dialog-border: seit 2026-09-28 ohne Wirkung (Dialoge haben Schatten statt Rahmen) */
  --vc-dialog-radius: 16px;               /* Eckenradius der Dialoge (Blatt von unten: +4px) */
  --vc-dialog-backdrop: rgba(0,0,0,0.78); /* Abdunklung hinter dem Dialog */
  /* --vc-slideshow-bg: #000;             Hintergrund der Slideshow-Seite (Default: --vc-bg) */
  --vc-input-bg: #181818;                 /* Fläche von Input/Select */
  --vc-input-border: #444444;             /* Rahmen von Input/Select */
  --vc-input-text: #eeeeee;               /* Textfarbe in Input/Select */
  --vc-input-radius: 4px;                 /* Eckenradius von Input/Select (Default = --vc-button-radius) */
  --vc-input-icon: #df3b00;               /* Chevron-Pfeil des Dropdowns (Default = --vc-accent) */
  --vc-input-focus: #df3b00;              /* Fokus-Ring + Hover-Rahmen von Input/Select (Default = --vc-accent) */
  --vc-input-selected-bg: #df3b00;        /* Aufgeklappte Liste: gewählter Eintrag (Default = --vc-accent) */
  --vc-input-selected-text: #ffffff;      /* … dessen Textfarbe (Default = --vc-text-on-accent) */
  --vc-input-option-hover: #3a3a3a;       /* Aufgeklappte Liste: Eintrag unter der Maus (Default = --vc-surface-hover) */
  --vc-label-text: #888888;               /* Beschriftung über den Feldern */
}

/* =============================================================================
   BEISPIEL: helles Marken-Theme (zum Vergleich auskommentiert).
   Zum Ausprobieren das obige :root entfernen und dieses aktivieren.
   ============================================================================*/
/*
:root {
  --vc-color-scheme: light;
  --vc-bg: #ffffff;
  --vc-surface: #ffffff;
  --vc-surface-raised: #f2f2f2;
  --vc-surface-hover: #e8e8e8;
  --vc-border: #e2e2e2;
  --vc-border-strong: #c2c2c2;
  --vc-text: #1a1a22;
  --vc-text-muted: #5e5e68;
  --vc-text-subtle: #8c8c94;
  --vc-text-on-accent: #ffffff;
  --vc-accent: #0066ff;
  --vc-accent-strong: #0047b3;
  --vc-accent-rgb: 0, 102, 255;
  --vc-accent-muted: #d6e4ff;
  --vc-header-bg: #ffffff;
  --vc-header-text: #1a1a22;
  --vc-header-text-muted: #6a6a74;
  --vc-header-border: #e2e2e2;
  --vc-header-border-width: 1px;
  --vc-button-bg: #0066ff;
  --vc-button-bg-hover: #0047b3;
  --vc-button-text: #ffffff;
  --vc-button-border: #0066ff;
  --vc-button-radius: 6px;
  --vc-button-inactive-bg: #e8e8e8;
  --vc-button-inactive-text: #1a1a22;
  --vc-slider-track: #e0e0e0;
  --vc-timeline-dim: rgba(0, 0, 0, 0.10);
  --vc-timeline-dim-text: var(--vc-text-muted);
  --vc-canvas-outline-color: #0066ff;
  --vc-canvas-outline-width: 2px;
}
*/
```

### All CSS variables at a glance

You don't have to set every variable – unset variables keep the simulator/tenant default. Colors accept any valid CSS color (`#hex`, `rgb()`, `hsl()`, named).

| Variable | Effect | Default |
| --- | --- | --- |
| `--vc-color-scheme` | light/dark scheme (`dark · light`) – affects native form elements among others | `dark` |
| `--vc-font-family` | font family of the entire UI (load custom webfonts via `@import` at the top of the theme CSS) | `system-ui, …` |
| `--vc-bg` | app background / canvas background while loading | `#0a0a0a` |
| `--vc-surface` | sidebar, dialog surfaces | `#181818` |
| `--vc-surface-raised` | raised elements (tracks, chips, inputs) | `#2a2a2a` |
| `--vc-surface-hover` | hover surface | `#3a3a3a` |
| `--vc-border` | default border color | `#2a2a2a` |
| `--vc-border-strong` | emphasized borders | `#444444` |
| `--vc-text` | default text color | `#eeeeee` |
| `--vc-text-muted` | secondary text | `#888888` |
| `--vc-text-subtle` | subtle hints | `#666666` |
| `--vc-text-on-accent` | text on accent/primary surfaces | `#ffffff` |
| `--vc-accent` | brand accent: active states, in/out marks, slider handles | tenant theme |
| `--vc-accent-rgb` | the same color as “R, G, B” (without `rgb()` parentheses) – used for translucent highlights; **must** match `--vc-accent` | tenant theme |
| `--vc-accent-strong` | reserved – currently read by no UI element | – |
| `--vc-accent-muted` | muted accent background | `#3a3a3a` |
| `--vc-danger` | delete: bin in the selection list, “Clear” tile, confirm button in delete prompts (selection only) | `#e5484d` |
| `--vc-warning` | warning: icon for empty advertising surfaces before export and sharing (selection only) | `#f5a524` |
| `--vc-hint-color` | tap hint: placeholders pulse once after loading | follows `--vc-accent` |
| `--vc-header-bg` | header background | `transparent` |
| `--vc-header-text` | header text (active product label, menu icon) | `#eeeeee` |
| `--vc-header-text-muted` | secondary header text (inactive product labels) | `#888888` |
| `--vc-header-border` | bottom divider of the header | `#1a1a1a` |
| `--vc-header-border-width` | divider width (`0px` = none) | `1px` |
| `--vc-header-padding` | header padding | `12px 20px` |
| `--vc-button-bg` | button surface | `#2a2a2a` |
| `--vc-button-bg-hover` | button surface on hover | `#3a3a3a` |
| `--vc-button-text` | button text color | `inherit` |
| `--vc-button-border` | button border | `#444444` |
| `--vc-button-radius` | button corner radius | `4px` |
| `--vc-button-inactive-bg` | surface of the **inactive** selector button (placeholder/scene selector) – deliberately neutral instead of accent, so active/inactive separate visibly | `#3a3a3a` |
| `--vc-button-inactive-text` | text color of the inactive selector button | `#eeeeee` |
| `--vc-slider-track` | track color of the sliders | `#3a3a3a` |
| `--vc-playhead-color` | playhead (line + handle) of **both** timelines – main timeline + placeholder mini bar | follows `--vc-accent` |
| `--vc-timeline-dim` | veil outside the in/out marks – adjust for light themes (see note) | `rgba(0, 0, 0, 0.55)` |
| `--vc-timeline-dim-text` | timecodes inside the veil – adjust for light themes (see note) | `#ffffff` |
| `--vc-canvas-outline-color` | outline color of the preview canvas | `#2a2a2a` |
| `--vc-canvas-outline-width` | outline width (`0px` = no frame) | `1px` |
| `--vc-canvas-radius` | rounded canvas corners (video, loading area and frame alike) | `0px` (square) |
| `--vc-spinner-color` | loading spinner: rotating arc | follows `--vc-accent` |
| `--vc-spinner-track` | loading spinner: ring/track | follows `--vc-border` |
| `--vc-spinner-size` | loading spinner: diameter | `32px` |
| `--vc-frame-color` | optional outer frame around the whole area: color | `transparent` |
| `--vc-frame-width` | optional outer frame: width (`0px` = off) | `0px` |
| `--vc-placeholder-bg` | background of the placeholder box (generated “A/B” texture) | `#ffffff` |
| `--vc-placeholder-text` | letter/label of the placeholder box | `#000000` |
| `--vc-placeholder-border` | border of the placeholder box | `#000000` |
| `--vc-dialog-bg` | surface of the dialogs (export, selection, catalog …) | follows surface |
| `--vc-dialog-radius` | corner radius of the dialogs (sheet from below: +4px) | `16px` |
| `--vc-dialog-backdrop` | dimming behind the dialog (slightly blurred) | `rgba(0,0,0,0.78)` |
| `--vc-slideshow-bg` | background of the slideshow page (slideshow enabled only) | follows `--vc-bg` |
| `--vc-input-bg` | input/select surface (incl. the effect dropdown on photo products) | follows surface |
| `--vc-input-border` | input/select border | follows border |
| `--vc-input-text` | text color in input/select | follows `--vc-text` |
| `--vc-input-radius` | input/select corner radius | follows `--vc-button-radius` (`4px`) |
| `--vc-input-icon` | chevron arrow of the dropdown | follows `--vc-accent` |
| `--vc-input-focus` | focus ring + hover border of input/select | follows `--vc-accent` |
| `--vc-input-selected-bg` | open list: background of the selected entry | follows `--vc-accent` |
| `--vc-input-selected-text` | open list: text color of the selected entry | follows `--vc-text-on-accent` |
| `--vc-input-option-hover` | open list: entry under the mouse | follows `--vc-surface-hover` |
| `--vc-label-text` | labels above the fields | follows `--vc-text-muted` |

- **No CORS needed:** the CSS file is loaded as a `<link>` (not via `fetch` like creatives) and may be cross-origin. CSS cannot run JavaScript.
- **URL-encode:** values for `?theme=`/`?bg=` must be URL-encoded (e.g. `%23` for `#`, `%2F` for `/`).
- **The loading spinner is themeable too:** `--vc-spinner-color` (rotating arc), `--vc-spinner-track` (ring) and `--vc-spinner-size` (diameter) – without an override it follows `--vc-accent` / `--vc-border` automatically.
- **Tap hint:** `--vc-hint-color` colors the one-time pulse of the placeholders after loading. Without an override it follows `--vc-accent`.
- **Delete and warning colors (selection enabled only):** `--vc-danger` (default `#e5484d`) colors the bin in the selection list, the “Clear” tile and the confirm button in delete prompts. `--vc-warning` (default `#f5a524`) colors the warning icon for empty advertising surfaces in the selection list and the notice in the “Current product” tab.
- **Rounded canvas corners:** `--vc-canvas-radius` (default `0px`) rounds the preview canvas, applying to video, loading area and frame alike. For example `--vc-canvas-radius: 16px;` in the `?theme=` CSS.
- **Color the playhead separately:** `--vc-playhead-color` colors the playhead of **both** timelines (main timeline + placeholder mini bar) independently of the accent; without an override it follows `--vc-accent`. Custom CSS rules on the `vc-main-playhead` class do **not** apply (the elements are styled inline) – use the variable.
- **⚠️ Timeline dimming must be adjusted for light themes:** the area outside the in/out marks is dimmed via `--vc-timeline-dim` (veil) and `--vc-timeline-dim-text` (the timecodes inside it). The defaults target **dark** themes (`rgba(0,0,0,0.55)` + white). For a **light** theme override **both** – a proven pair: `--vc-timeline-dim: rgba(0, 0, 0, 0.10);` and `--vc-timeline-dim-text: var(--vc-text-muted);`.
- **Dialogs & input fields are themeable too:** the dialogs (`--vc-dialog-bg`, `--vc-dialog-radius`, default 16px, `--vc-dialog-backdrop` = slightly blurred dimming behind them; they carry a soft shadow instead of a border) and all input/select fields incl. the photo-effect dropdown (`--vc-input-bg`, `--vc-input-border`, `--vc-input-text`, `--vc-input-radius`, `--vc-input-icon` = dropdown chevron, `--vc-input-focus` = focus ring, `--vc-input-selected-bg`/`--vc-input-selected-text` = selected entry in the open list, `--vc-input-option-hover` = entry under the mouse, `--vc-label-text`) can be styled individually – without an override they follow the surface/accent variables; the corner radius follows `--vc-button-radius`.
- **Only placeholder colors?** You don't need a theme file for that: `?placeholderBg=`/`?placeholderText=`/`?placeholderBorder=` set the placeholder box directly via URL (see URL parameters above) and override the theme + `?theme=`.
- You'll find every overridable CSS variable in the table and the theme template below.

## Step 8 — Reference: postMessage API

Build your own UI and drive the iframe via `window.postMessage`. The iframe also sends events back (including the list of available product codes).

### Incoming commands (your page → iframe)

#### Product, stage and view

| Message | Effect |
| --- | --- |
| `{ type: "kreativsimulator:setProduct", code: "ec5472c2", stage? }` | Loads the product with this code. The optional `stage` (`"auto"`, `"1024x1024"` or `{ width, height }`) sets the canvas shape at the same time; without it the current stage is kept. With the product library any code of your catalog works, also one that is not in `ready.products`. |
| `{ type: "kreativsimulator:setStage", stage: "auto" \| "WxH" \| { width, height } }` | Sets the **stage** (shape and size of the canvas) at runtime. `"auto"` = film/clip 16:9, photo in its own aspect ratio; `WxH` = fixed shape + display size for every product. Reply: `stageChanged`. The stage **survives** product switches until you set it again. |
| `{ type: "kreativsimulator:setEffect", id: "static-5s" }` | **Photo products:** selects an entry from `ready.effects` – a static preset (`static-5s`/`static-10s`, always present) or a named Ken-Burns effect. `id: null` = static still (1 frame). Changes the timeline length → a `playbackState` follows. |
| `{ type: "kreativsimulator:setView", view: "0e23675447" }` | **Only with the view picker enabled** (no effect otherwise): shows the current photo product in this view (key from `ready.views`/`productChanged.views`). `view: null` = default view. The creatives stay in place. Reply: `viewChanged`. |
| `{ type: "kreativsimulator:getEffects", code: "ec5472c2" }` | Queries the effect list of **any** product **without** activating it (live view stays unchanged) → reply as an `effects` event. For a per-product effect picker before calling `setProduct`. |

#### Creatives

| Message | Effect |
| --- | --- |
| `{ type: "kreativsimulator:setCreative", url: "https://…/creative.png" }` | Places the image/video (any public `http(s)` URL) as the creative on **all** placeholders. |
| `{ type: "kreativsimulator:setCreative", url: "…", loader: "Loader A" }` | Fills **only** the named placeholder. |
| `{ type: "kreativsimulator:setCreative", url: "https://…/live.m3u8", kind?: "stream" }` | Places a **video stream** as the creative – HLS (`.m3u8`) or DASH (`.mpd`), live or on demand. `kind` (`"image"` \| `"video"` \| `"stream"`) is **optional**: without it the simulator detects the type from the file extension or content type. Reply: `creativeLoaded`. |
| `{ type: "kreativsimulator:clearCreative", loader?: "Loader A" }` | Resets one (or without `loader` all) placeholders to the default. |

#### Playback

| Message | Effect |
| --- | --- |
| `{ type: "kreativsimulator:play" }` · `pause` · `stop` | Starts / pauses / stops (back to start) playback. |
| `{ type: "kreativsimulator:stepForward", frames?: 1 }` · `stepBackward` | Steps `frames` frames forward/back (default 1) and pauses. |
| `{ type: "kreativsimulator:seek", frame: 120 }` | Jumps to an absolute frame index (`0…frameCount-1`). |
| `{ type: "kreativsimulator:getState" }` | Requests the current playback state → reply as `playbackState` (with `view` when the view picker is enabled). |

#### Export

| Message | Effect |
| --- | --- |
| `{ type: "kreativsimulator:export", filename?, width?, height?, watermark?, logo? }` | Starts the MP4 export. With `width`+`height` directly at **any** resolution; without, the export dialog opens. `watermark: true` adds the “Vorschau” (preview) watermark, `logo: false` leaves the tenant logo out (see notes). |
| `{ type: "kreativsimulator:exportImage", filename?, width?, height?, watermark?, logo?, original? }` | Saves a **still image** as JPG (directly, no dialog). **Video products:** the currently shown frame at `width`×`height` (default 1280×720, 16:9). **Photo products:** the **whole photo** (no 16:9 crop, no Ken-Burns) with the placed creatives at the photo's aspect ratio, long side 1280 px (landscape e.g. 1280×961, portrait 720×1280). With `original: true` and **photo export in original size** enabled, at the photo's full resolution (at most 8192 px, selectable as the “Original” resolution in the dialog). `width`/`height` are ignored for photos. `watermark` and `logo` as with `export`. |

#### Interface and language

| Message | Effect |
| --- | --- |
| `{ type: "kreativsimulator:setLanguage", lang: "fr" }` | Switches the UI language (de/en/fr/it). |
| `{ type: "kreativsimulator:setChrome", header?, sidebar?, playback?, timeline?, export?, effects?, views?, selection?, library?, hint?, editor? }` | Toggles individual UI areas at runtime (each `true`/`false`). `effects` = effect picker (photo products, button on the image), `views` = “Views (n)” button on the image (only with the view picker enabled), `selection` = selection buttons (only with the selection enabled), `library` = “Catalog (n)” button (only with the product library enabled). `hint` = tap hint, `editor` = `"auto"`, `"sidebar"` or `"overlay"` (where a tap on the placeholder leads). These two are not part of `chromeChanged`. |
| `{ type: "kreativsimulator:fullscreen", enabled? }` | Fullscreen: `true`=on, `false`=off, without `enabled`=toggle (see note). |

#### Access (token only)

| Message | Effect |
| --- | --- |
| `{ type: "kreativsimulator:setAccessToken", token: "vcke_…" }` | **Only with “Token only” access** (step 9): hands over a new embed token, e.g. after `accessRequired` or after a login. The products then load without a reload. Invalid or already used → `error` with `code: "ACCESS_TOKEN_INVALID"`. |

```js
// Save a specific frame of a video product as JPG: pause/seek first, then exportImage.
send({ type: "kreativsimulator:seek", frame: 120 });
send({ type: "kreativsimulator:exportImage", filename: "poster", width: 1920, height: 1080 });

// Export WITHOUT the tenant logo (top left): the field wins over the URL.
// ?logo=0 on the iframe URL switches it off for every export of this embed.
send({ type: "kreativsimulator:export", filename: "creation", width: 1920, height: 1080, logo: false });
send({ type: "kreativsimulator:exportImage", filename: "poster", logo: false });
```

### Example: live stream as the creative

A **live stream** (HLS `.m3u8` or DASH `.mpd`) plays directly on the poster area. That lets you show a DOOH programme or a live broadcast at the location, for example. The URL in the example is a public test stream, which you can try in the live demo above with “Send live stream”.

```js
// A live stream as the creative: HLS (.m3u8) or DASH (.mpd), live or on demand.
// Any public http(s) URL works — no CORS needed, the simulator falls back to its
// own media proxy. Public HLS live test stream (Unified Streaming):
send({
  type: "kreativsimulator:setCreative",
  url: "https://demo.unified-streaming.com/k8s/live/stable/scte35.isml/.m3u8",
  loader: "Loader A", // optional — without it, all placeholders
  // kind: "stream",  // only needed if the URL has no .m3u8 / .mpd ending
});

window.addEventListener("message", (e) => {
  const m = e.data;
  if (!m || m.source !== "kreativsimulator") return;
  if (m.type === "creativeLoaded") console.log(m.kind, m.live ? "LIVE" : "VOD"); // "stream" LIVE
  if (m.type === "error") console.warn(m.message); // e.g. "hls …" if the stream fails
});
```

### Creatives from the VirtualCampaign asset management

Assets uploaded to your tenant's VirtualCampaign account can be used directly as creatives – **without** hosting them anywhere again and **without** any CORS configuration on your side.

You get the URLs ready-made: open the asset preview in the VirtualCampaign web UI and click the **chain icon** – it puts all URLs of the creative incl. their sizes on the clipboard, ready to paste into an e-mail.

The URLs look like this:

```text
https://virtualcampaign-prod.nbg1.your-objectstorage.com/assets-public/<token>/web_720.webp
```

```js
// A published asset from the VirtualCampaign asset management — same host as
// the simulator, so no CORS setup on your side. Pick the web_* variant you need.
send({
  type: "kreativsimulator:setCreative",
  url: "https://virtualcampaign-prod.nbg1.your-objectstorage.com/assets-public/7f3a…/web_720.webp",
  loader: "Loader A",
});
```

| Variant | Size | When |
| --- | --- | --- |
| `web_720.mp4` · `web_720.webp` | up to 1280 × 720 | default – best quality |
| `web_640.mp4` · `web_640.webp` | up to 1138 × 640 | compromise |
| `web_320.mp4` · `web_320.webp` | up to 570 × 320 | slow connection, many creatives, mobile |

- **Extension follows the asset type:** video assets serve `.mp4`, image assets `.webp`. Image variants keep a **transparent background** (cut-out creatives), which JPEG would lose.
- **The URLs open in the browser** – handy for checking before you build them in. They are not guessable: the token in the URL is the secret. Treat them as confidentially as the creative itself.
- **Revoking:** “Unshare” in the same menu deletes the published files; the URLs return an error afterwards. If the same creative is shared again later, the **same** URLs come back – existing embeds work again.
- **Why three sizes?** The simulator downloads an image or video creative **completely** before it appears (only HLS/DASH streams play segment by segment). File size = waiting time. A placeholder is usually just a small part of the video – the small variant is often enough. When in doubt use `web_320` for the live preview and `web_720` only for the export.
- **Aspect ratio is preserved:** the variants are fitted proportionally into the box, never distorted, never upscaled (portrait e.g. 404 × 720, a 2.96:1 billboard creative 1280 × 432). Smaller source files stay unchanged.
- **New uploads only:** the variants are created on upload. Older assets don't show the chain icon – re-upload them once.
- **The original file is deliberately not served** (may be e.g. ProRes in a `.mov` and up to 500 MB). Always use a `web_*` variant.

### Outgoing events (iframe → your page)

```js
{ source: "kreativsimulator", v: 1, type: "ready",
  product: "ec5472c2", locale: "de",
  products: [{ code: "ec5472c2", name: "City-Light …" }, …],
  // product library only: products = the first 30, the total count is here
  productsTotal: 240,
  loaders: [{ name: "Loader A", aspectRatio: 2.96 }, …],
  // shape of the current stage (backing: long side 1280), stage mode and
  // export presets relative to that shape (short side 360/720/1080)
  render: { width: 1280, height: 720, aspectRatio: 1.7778, stage: "auto",
            presets: [{ label: "Small (640×360)", width: 640, height: 360 },
                      { label: "Standard (1280×720)", width: 1280, height: 720 },
                      { label: "Large (1920×1080)", width: 1920, height: 1080 }] },
  // photo products (productType "bild"): effects[] = two always-present static
  // presets (static-5s, static-10s) + named Ken-Burns effects; default = static-5s.
  // video products report isPhoto:false, effects:[], selectedEffect:null.
  isPhoto: true,
  effects: [{ id: "static-5s", name: "Static (5s)", durationFrames: 125 },
            { id: "static-10s", name: "Static (10s)", durationFrames: 250 },
            { id: "fx-a", name: "Effect A", durationFrames: 125 }, …],
  selectedEffect: "static-5s" }

{ source: "kreativsimulator", v: 1, type: "productChanged", code: "ec5472c2",
  loaders: [{ name: "Loader A", aspectRatio: 2.96 }, …],
  // the shape can change with the product (a photo is not 16:9) → resize your iframe box
  render: { width: 1280, height: 854, aspectRatio: 1.4988, stage: "auto", presets: [ … ] },
  isPhoto: true,
  effects: [{ id: "static-5s", name: "Static (5s)", durationFrames: 125 },
            { id: "static-10s", name: "Static (10s)", durationFrames: 250 },
            { id: "fx-a", name: "Effect A", durationFrames: 125 }, …],
  selectedEffect: "static-5s" }

// reply to getEffects {code}: a product's effect list WITHOUT activating it.
// same fields as productChanged (incl. the static presets), plus code.
// video products report isPhoto:false, effects:[], selectedEffect:null.
{ source: "kreativsimulator", v: 1, type: "effects", code: "ec5472c2",
  isPhoto: true,
  effects: [{ id: "static-5s", name: "Static (5s)", durationFrames: 125 },
            { id: "static-10s", name: "Static (10s)", durationFrames: 250 },
            { id: "fx-a", name: "Effect A", durationFrames: 125 }, …],
  selectedEffect: "static-5s" }

// playback state: on play/pause, ~4×/s while playing, after seek/stop/step, and on getState
// for photo products, frameCount equals the active effect's duration
{ source: "kreativsimulator", v: 1, type: "playbackState",
  frame: 120, playing: true, frameCount: 250, fps: 25 }

// confirmation after setChrome, with the new visibility state.
// views / selection / library are added only when the view picker, the selection or the library is enabled.
{ source: "kreativsimulator", v: 1, type: "chromeChanged",
  header: false, sidebar: false, playback: true, timeline: true, export: false, effects: false }

// fullscreen state — on every change (e.g. ESC); for syncing your own button
{ source: "kreativsimulator", v: 1, type: "fullscreenChanged", fullscreen: true }

// the stage (canvas shape) changed: after setStage, setProduct{stage} or a product
// switch with a different aspect ratio. Same fields as the render block.
{ source: "kreativsimulator", v: 1, type: "stageChanged",
  width: 1280, height: 1280, aspectRatio: 1, stage: "explicit", presets: [ … ] }

// confirmation after a successful setCreative. kind = detected type,
// live: true = live stream (not seekable).
{ source: "kreativsimulator", v: 1, type: "creativeLoaded",
  url: "https://…/live.m3u8", loader: "Loader A", kind: "stream", live: true }

{ source: "kreativsimulator", v: 1, type: "error", message: "…", code, loader }

// views enabled only: the shown view changed (end customer in the iframe, setView).
// fallback: true = the requested view does not exist (any more) → default view shown.
// Pass `view` as `impressionKey` when you order.
{ source: "kreativsimulator", v: 1, type: "viewChanged",
  code: "ec5472c2", view: "0e23675447", fallback: false }

// selection enabled only: its content after every change and after it is restored
// from the browser. param = the full state as the value for ?sel= ("" = empty).
{ source: "kreativsimulator", v: 1, type: "selectionChanged", count: 2,
  items: [{ code: "ec5472c2", view: null, name: "City-Light …", ready: true }, …],
  param: "1.eyJ…" }

// share link created (copied = on the clipboard). param = the value for ?sel=.
{ source: "kreativsimulator", v: 1, type: "selectionLinkCopied",
  url: "https://demo.kreativsimulator.com/?sel=1.eyJ…", param: "1.eyJ…", copied: true }

// slideshow created (slideshow enabled only).
{ source: "kreativsimulator", v: 1, type: "slideshowCreated",
  url: "https://demo.kreativsimulator.com/s/Abc…", expiresAt: "2026-10-03T12:00:00.000Z" }

// "Token only" access: products are missing because of the access.
// "missing" = no token, "expired" = token/session expired or already used,
// "denied" = no access. Answer: a new token via setAccessToken.
{ source: "kreativsimulator", v: 1, type: "accessRequired", reason: "expired" }
```

Always filter incoming messages on `source === "kreativsimulator"`. `v` is the protocol version (currently 1). **New message types and fields are added without raising `v`.** So ignore types and fields you don't know, and don't check for exact object shapes. A new version (`v: 2`) only comes with an incompatible change, and then alongside `v: 1`.

### Views (perspectives of a photo product)

Some photo products are shot from **several perspectives**, for example a wide shot and a close-up of the same advertising site. If the **view picker is enabled for your tenant** (ask us), the end customer sees a **“Views (n)”** button at the top left of the image for such products. A click opens a dialog with previews and names, and only then do the images load. On phones and touch devices it opens full screen like the product catalog. The chosen view loads and their creatives stay in place. When space is short, the button shows only a layers icon with the count. Without it, nothing changes.

The demo tenant has the view picker enabled. In the live demo above the “Views” block appears as soon as a photo product with several perspectives is active.

With the picker enabled, `ready` and `productChanged` carry two extra fields, `views` and `view`:

```js
// ready and productChanged carry two extra fields when views are enabled:
views: [{ key: "6c02edffc6", name: "Wide shot", isDefault: true,
          visibleLoaders: ["Loader A"],            // placeholders visible in this view
          previewUrl: "/api/vc/v1/products/ec5472c2/impressions/6c02edffc6/creative?w=320&kind=preview" },
        { key: "0e23675447", name: null, isDefault: false, … }],
view: "6c02edffc6"                                  // the view shown now

// Products without views (film, classic photo products): views: [], view: null.

// Your own picker: hide the "Views (n)" button (?views=0), show the previews, switch:
send({ type: "kreativsimulator:setChrome", views: false });
const img = new URL(v.previewUrl, iframe.src).href;  // previewUrl is relative to the simulator
send({ type: "kreativsimulator:setView", view: "0e23675447" });  // view: null = default view
```

- **Every view has its own settings:** VirtualCampaign keeps effects (colour, blur, glow, reflection, frame), crop and Ken Burns effects per view, for example for day, night or winter. When the view changes, the simulator loads its values. Creatives and their placement stay. If the end customer changes effects, the simulator remembers that per view. So `effects` in `ready`/`productChanged` belongs to the view shown.
- Products without views (film, classic photo products) report `views: []` and `view: null`.
- **Ordering:** pass the shown view to the VirtualCampaign API as `impressionKey` when you place the order (`POST /v1/orders`). You find it in `view` from `ready`/`productChanged`, in the last `viewChanged` or in `getState`. Without `impressionKey` the default view is rendered.
- `previewUrl` can carry a `&v=…`: the version of the preview image. It changes after the view is adjusted in VirtualCampaign, so take the URL over unchanged.
- **Your own picker:** hide the “Views (n)” button with `?views=0` or `setChrome { views: false }`, show the previews from `previewUrl` and switch with `setView`.

### Complete example

Build your own buttons from the product list, place a creative on a targeted placeholder (correct ordering), and a custom resolution picker from `render.presets`:

```html
<iframe id="ks" src="https://demo.kreativsimulator.com/"
        style="width:100%;height:720px;border:0" allow="fullscreen"></iframe>

<script>
  const iframe = document.getElementById("ks");
  const send = (msg) => iframe.contentWindow.postMessage(msg, "*");

  window.addEventListener("message", (e) => {
    const m = e.data;
    if (!m || m.source !== "kreativsimulator") return;

    if (m.type === "ready") {
      // Build your own buttons from the product list
      for (const p of m.products) {
        const btn = document.createElement("button");
        btn.textContent = p.name;
        btn.onclick = () =>
          send({ type: "kreativsimulator:setProduct", code: p.code });
        document.body.appendChild(btn);
      }
      // Build your own resolution picker + export button from m.render.presets
      const sel = document.createElement("select");
      for (const r of m.render.presets) {
        const o = document.createElement("option");
        o.value = r.width + "x" + r.height;
        o.textContent = r.label;
        sel.appendChild(o);
      }
      const exportBtn = document.createElement("button");
      exportBtn.textContent = "Export";
      exportBtn.onclick = () => {
        const [width, height] = sel.value.split("x").map(Number);
        send({ type: "kreativsimulator:export", filename: "creation", width, height });
      };
      document.body.append(sel, exportBtn);
    }

    // The product is active now → only here may you target a placeholder.
    if (m.type === "productChanged") {
      const first = m.loaders[0];                // take the name from the event
      if (first) {
        send({
          type: "kreativsimulator:setCreative",
          url: "https://cdn.your-agency.com/creative.png",
          loader: first.name,                    // this placeholder only
        });
      }
    }

    if (m.type === "error") console.warn("KS error:", m.message);
  });

  // Creative on ALL placeholders (no loader) — fine right after `ready`:
  // send({ type: "kreativsimulator:setCreative", url: "https://cdn.your-agency.com/creative.png" });
</script>
```

### Important

- **Ordering on product change:** after `setProduct` the new product is only active once the `productChanged` event arrives. Send placeholder-targeted commands (`setCreative`/`clearCreative` with `loader`) only afterwards – before that the iframe only knows the old product's placeholders. A product change also resets all creatives.
- **Your own controls:** playback (`play`/`pause`/`stop`/`step…`/`seek`), `export` and `exportImage` always work – whether or not the built-in buttons are visible. Hide them with `?embed=kiosk` and drive everything yourself.
- **Pick the export resolution:** `export` with `width`+`height` renders at any resolution. The current render shape (`width`/`height`/`aspectRatio` of the stage) and three matching presets (“Small/Standard/Large”, short side 360/720/1080 px) come from the `render` field in `ready`, `productChanged` and `stageChanged` – build your own resolution picker from it. Tip: keep `render.aspectRatio` or you get bars.
- **Stage = shape of the canvas:** `setStage` (or `?stage=`) decides the canvas shape. `"auto"` means film/clip 16:9 and photo in its own aspect ratio, `"1024x1024"` forces one fixed shape for every product. Every change emits a `stageChanged` event with `width`/`height`/`aspectRatio`. Feed it into the `aspect-ratio` of your iframe box and no letterbox is left.
- **Playback mode of video creatives:** a video creative runs in **real time** by default (independent of the product timeline). In the sidebar the user can switch each placeholder to **synced start** (the creative restarts whenever the placeholder appears) or to **still frame**, plus a separate **loop** switch. The MP4 export follows the chosen mode. There is currently **no** postMessage command for this – with the sidebar hidden (`?sidebar=0`/`?embed=kiosk`) it stays real time + loop. Talk to us if you need to drive it remotely.
- **Save a still image (JPG):** besides the video, **every** product can save a still – in the built-in export dialog via the “Video” and “Image” buttons with a free file name and the “Download” button, or via the `exportImage` message. **Video products:** the currently shown frame (playhead) incl. creatives at the chosen resolution (default 1280×720). **Photo products:** the whole photo with creatives at the photo's aspect ratio, long side 1280 px. The dialog lets you pick the size here too: small, standard and, when enabled, “Original” (full resolution, at most 8192 px). For a specific frame send `pause`/`seek` first, then `exportImage`. The logo rules (`?logo`, `logo` field) and the watermark rules apply as for MP4.
- **Switch the logo off in the export:** by default every exported MP4 and JPG carries the tenant logo top-left. Some tenants have “no logo” set as their default (talk to us if you want that). Two ways: `?logo=0` in the iframe URL leaves it out of **all** exports of this embed – including those from the built-in export dialog. The `logo` field on `export`/`exportImage` applies to **that** export only (`false` = without, `true` = with, even when `?logo=0` is set; omitted = the value from the URL, otherwise the tenant default). `export` **without** `width`/`height` opens the dialog. There only `?logo` counts, not the message field. Try it in the live demo above with the “Logo in the export” switch.
- **Add the watermark:** `export`/`exportImage` with `watermark: true` composites the diagonally tiled “Vorschau” (preview) watermark into the exported MP4 or JPG – e.g. for non-binding previews. The flag is **additive only**: `false` (or omitted) gives the normal behavior; on the **demo tenant** the watermark is always on and cannot be disabled. To mark **all** exports of an embed (including those from the built-in export dialog), use the URL parameter `?watermark=1` instead.
- **Photo products & Ken-Burns effects:** some products are still images with named camera moves. `ready`/`productChanged` then report `isPhoto: true` and a list of `effects` (`{ id, name, durationFrames }`). The list **always** starts with two static presets (`static-5s` = 5 s, `static-10s` = 10 s), followed by the named effects; on load `static-5s` is selected automatically (`selectedEffect`). Use `setEffect { id }` to switch entry (or `id: null` for a pure still image, 1 frame). Playback and export work just like video; `playbackState.frameCount` equals the active entry's duration.
- **Pre-fetch effects (without switching product):** `ready`/`productChanged` only carry the effects of the **active** product. To build an effect picker for **other** products without switching the live view, send `getEffects { code }` and listen for the `effects` event (same fields as `productChanged`, plus `code`). That gives you each effect's `id`/`name` to drive later via `setEffect` (after `setProduct` to that code).
- **Turn fullscreen on reliably:** browsers only allow enabling it as a direct reaction to a user action. A `fullscreen` postMessage usually doesn't count as one and may be blocked. So call `iframe.requestFullscreen()` **on your page** on click (the iframe needs `allow="fullscreen"`); the postMessage is fine for off/toggle. The state comes via `fullscreenChanged`.
- **Placeholder names** are not free-form – use exactly one `name` from the `loaders[]` array of `ready` or `productChanged`.
- **CORS is not required:** the iframe first loads the URL directly. If your host sends no `Access-Control-Allow-Origin`, the simulator loads the file or stream through its **own media proxy** automatically. With CORS it is a little faster. The proxy only loads **public** addresses (ports 80/443, no IP addresses or internal names) and never forwards cookies or credentials – URLs that need a login do not work. Allowed are `http(s)` and `data:` URLs.
- **Video streams (HLS/DASH):** `setCreative` also accepts `.m3u8` and `.mpd` addresses. On-demand streams behave like video files. **Live streams** always run in real time, without in/out, synced start, still frame or loop (the sidebar shows “LIVE”); `creativeLoaded` reports `live: true`. In the export the placeholder shows the stream picture that is playing while it renders. This is **not** frame-exact and cannot be repeated. For URLs without a clear extension (e.g. `…/stream?id=42`) send `kind: "stream"`.
- **Security:** which pages may embed the iframe is set server-side via `frame-ancestors` – only those can send messages at all.

## Step 9 — Optional: Token-only access (login or paywall)

By default anyone can open the Creative Simulator on your subdomain. If it should only appear for signed-in or paying visitors, your tenant sets the subdomain to **“Token only”** in VirtualCampaign under *Settings → Kreativsimulator*. From then on the simulator shows a notice instead of products when there is no valid token.

**The principle:** the API key stays on your **server**. Your server fetches a short-lived **embed token** for every page view and passes only that token to the iframe.

### Prerequisite: create an API key (once)

1. In VirtualCampaign: *Settings → REST API → New key*.
2. Choose **type “Kreativsimulator”** and assign a **user group**. The group decides which products your visitors see.
3. Copy the key. It is shown **only once**. Keep it on your server only (e.g. as an environment variable), never in HTML, browser JavaScript or a repository.

> If the key is deactivated or deleted, no new tokens are issued and all running sessions end at once. Tip: create a separate key just for this purpose.

### Step 1: fetch a token (on your server)

`POST https://api.virtualcampaign.dev/v1/kreativsimulator/embed-tokens`

| Body field | Required | Meaning |
| --- | --- | --- |
| `site` | no* | Your subdomain, e.g. `"demo"` for `demo.kreativsimulator.com`. *Required if your tenant has several subdomains. |
| `ttlSeconds` | no | Token lifetime in seconds, 60 to 900. Default 300 (5 minutes). |

```bash
curl -X POST https://api.virtualcampaign.dev/v1/kreativsimulator/embed-tokens \
  -H "Authorization: Bearer <API-KEY>" \
  -H "Content-Type: application/json" \
  -d '{"site":"<your-tenant>"}'
```

Response (`201`):

```json
{
  "token": "vcke_…",
  "expiresAt": "2026-09-25T12:05:00.000Z",
  "site": "<your-tenant>",
  "embedUrl": "https://<your-tenant>.kreativsimulator.com/#vcToken=vcke_…"
}
```

Node.js example (a server route your page calls while rendering):

```js
// Call this only for signed-in visitors. You check that as usual.
async function kreativsimulatorEmbedUrl() {
  const res = await fetch("https://api.virtualcampaign.dev/v1/kreativsimulator/embed-tokens", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.VC_KREATIVSIMULATOR_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ site: "<your-tenant>" }),
  });
  if (!res.ok) throw new Error(`Kreativsimulator token: ${res.status}`);
  const { embedUrl } = await res.json();
  return embedUrl;
}
```

### Step 2: embed the iframe

Use `embedUrl` as the `src`. Further URL parameters (step 3) go **before** the `#`:

```html
<iframe src="https://<your-tenant>.kreativsimulator.com/?lang=de&product=ec5472c2#vcToken=vcke_…"
        title="Kreativsimulator" style="width:100%;height:720px;border:0" allow="fullscreen"></iframe>
```

The simulator takes the token from the address right away and exchanges it for a **session** (default 4 hours, configurable per tenant).

### Step 3 (optional): extend the session without a reload

When the session expires or the token is missing, the simulator sends `accessRequired`. Your page then fetches a new token through your server and sends it with `setAccessToken`:

```js
window.addEventListener("message", async (e) => {
  if (e.data?.source !== "kreativsimulator" || e.data.type !== "accessRequired") return;
  const { token } = await fetch("/my-server/kreativsimulator-token").then((r) => r.json());
  iframe.contentWindow.postMessage({ type: "kreativsimulator:setAccessToken", token }, "*");
});
```

### Important

- A token is valid for **5 minutes** (or `ttlSeconds`) and **only once**. Fetch it when you render the page, anew for **every** page view. Don't fetch tokens in advance and don't cache them. A copied iframe address stops working after that.
- Don't write tokens or sessions to logs or analytics.
- The token sits after `#` on purpose, not after `?`. That keeps it away from servers, logs and the `Referer`.
- The session sees exactly the products of the API key's user group.

### Errors when fetching a token

| HTTP | `code` | Meaning |
| --- | --- | --- |
| 401 | `INVALID_API_KEY` | Key missing, wrong or deactivated. |
| 403 | `KREATIVSIMULATOR_REQUIRED` | Key is not of type “Kreativsimulator”. |
| 403 | `GROUP_REQUIRED` | Key has no user group. |
| 403 | `FEATURE_DISABLED` | REST API or Kreativsimulator not enabled for the tenant. |
| 404 | `SITE_NOT_FOUND` | Subdomain unknown, inactive or not part of your tenant. |
| 400 | `SITE_REQUIRED` | Tenant has several subdomains. Pass `site`. |
| 429 | `RATE_LIMIT` | Too many requests. Respect `Retry-After`. |

## Step 10 — Optional: Selection, share link and slideshow

We enable these features for your subdomain (ask us). Without them, nothing changes in the iframe.

- **Selection:** the **＋ “Add”** button on the right of the bar, before the export button, adds the current product with its creatives, its effect and its length, as a finished image like an export and with the same logo and watermark rules. **Selection** on the right of the bar (count as a badge; with the product library the “My selection” tab at the top with total duration and count instead) opens the “Export and share” dialog in the “My selection” tab. When the selection has entries, the export button opens there too. The user sorts the entries by dragging (phone: hold, then drag; also in the “My selection” bar), **shares the selection** by link, creates a **selection video** (all entries one after another in one MP4, up to 720p, with music if wanted, which lives on the simulator under `/music/`) and downloads, under **Download**, “Images + PDF” (ZIP with medium-resolution images and an A4 PDF with name and product code per page) or a **PowerPoint** file (16:9, title slide and one slide per entry). When the user opens an entry and changes effect, in/out or creatives, the entry updates itself. With the product library the user can play the **whole selection** in a row. ZIP and PowerPoint follow `?export`: without the export button they are not offered. At most 20 entries. The link carries only products, views and creatives **by URL**. Creatives from files are missing for the recipient.
- **Clipboard in the iframe:** for “Copy link” to work, allow it on the iframe with `allow="clipboard-write; fullscreen"`. Without it the simulator shows the link for manual selection.
- **Your page as the link target:** the `selectionLinkCopied` event carries `param`, the value for `sel`. Append it to your iframe's `src` (`…?sel=<param>`) and the selection opens on your page. With “Token only” access this is required, because the direct link does not open without a token.
- **Slideshow (enabled separately):** the selection becomes a shareable link `https://<subdomain>.kreativsimulator.com/s/<hash>`, optionally with a **title** and a multi-line **message**, valid for 1 to 30 days. The user picks “Image slideshow” or “Video”. The image slideshow opens with a start screen (title, message, “Start” button) that waits until the recipient taps, and plays the images with crossfades. Photo products run with the crop, Ken Burns effect and length they have in the simulator. With “Video” the selection video is created and the link plays it. Both kinds can have music, which can be previewed in the dialog. The page has a mute button, and the video also has stop and a timeline to scrub. A share button copies the link so the recipient can pass it on right away. Fullscreen and swiping work on phones too. The link is public, the hash is the access, also with “Token only” access. In the dialog the user enters their own e-mail address and the recipient's. The recipient gets the link with the message by e-mail (replies go to the sender), never the delete link. VirtualCampaign stores the finished images until the link expires. The creator gets an e-mail with a view link and a delete link. Event: `slideshowCreated {url, expiresAt}`.
- **The selection is kept:** the simulator keeps the selection, finished images included, for 30 days in the visitor's browser. Navigating or reloading your page does not clear it. You need to do nothing and pass no session. Every visitor has their own selection, and browsers keep the storage separate per embedding website. None of it goes to a server. Limits: same browser on the same device only, and a private window forgets it when closed.
- **Selection events:** `selectionChanged {count, items: [{code, view, name, ready}], param}` arrives after every change and after a restore. `param` is the full state as the value for `sel`. To keep the selection across devices, e.g. in the user account, store it and later append it to the iframe `src` (`…?sel=<param>`). Creatives from files are missing then, and the entries are loaded again. Hide the buttons: `?selection=0` or `setChrome {selection: false}`.
- **Keyboard:** while a dialog is open (selection, slideshow, views, export) or the user types in a field, the simulator's shortcuts are off (space = play/pause, ←/→, i/o).

Allow “Copy link” and keep the selection in the user account:

```html
<iframe id="ks" src="https://demo.kreativsimulator.com/"
        title="Kreativsimulator" style="width:100%;height:720px;border:0"
        allow="clipboard-write; fullscreen"></iframe>
<script>
  window.addEventListener("message", (e) => {
    if (e.data?.source !== "kreativsimulator") return;
    // the full selection as the value for ?sel= — store it, e.g. in the user account
    if (e.data.type === "selectionChanged") saveSelection(e.data.param);   // your function
    if (e.data.type === "selectionLinkCopied") console.log("share link:", e.data.url);
    if (e.data.type === "slideshowCreated") console.log("slideshow until", e.data.expiresAt);
  });
  // later, on any device: open the stored selection on your page
  // document.getElementById("ks").src =
  //   "https://demo.kreativsimulator.com/?sel=" + encodeURIComponent(storedParam);
</script>
```

## Step 11 — Optional: Product library: catalog with search and filters

We enable the product library per customer (ask us). Without it, nothing changes in the iframe.

- **Header:** the **“Catalog (n)”** button opens a dialog with search and filters: type (photo, film, clip), format (landscape, portrait, other), aspect ratio, advertising medium and location. The numbers next to the filters show how many products match. The bar below shows the filter's hits (without a filter, all products) and loads more while scrolling. The “Products (n)” tab shows the number of hits. With the selection enabled there are two tabs, “Products” and “My selection”, and the selection button at the bottom goes away.
- **Catalog:** products already in “My selection” carry an “In selection” badge. On phones the filters open their own panel with “Show n products”. Advertising medium and location come from the product tags in VirtualCampaign.
- **Loading on demand:** the simulator loads products page by page. `ready.products` holds the first 30 products, the total count is in `ready.productsTotal`. `setProduct` and `getEffects` work with **any** code of your catalog. For your own product list, fetch the products on your server from the VirtualCampaign API, as in the gallery example.
- **Swiping on phones and tablets:** swiping left shows the next product (in the “My selection” tab the next entry), swiping right the previous one. The image follows the finger. The iframe takes horizontal swipe gestures for this (`touch-action: pan-y`), vertical scrolling of your page still works.
- **Together with the selection:** when the user places a creative or confirms changes in the editor on the image with ✓, the product lands in “My selection” and the header shows that tab. Creatives sent via `setCreative` do not trigger this. In the catalog the user ticks several products and adds them without a creative with “Add to ‘My selection’”. A “Clear” tile empties the selection after a confirmation. Entries with empty advertising surfaces are marked, and the simulator asks before export or sharing. In the editor on the image, ⋯ “Creative in all products of the selection” copies the creative to every surface of every entry after a warning. `selectionChanged` reports each of these changes.
- **Hide the button:** `?library=0` or `setChrome {library: false}`. Bar and tabs stay.

## Good to know

- **No API key in the browser.** Authentication against the VirtualCampaign API runs entirely server-side. Without “Token only” you configure nothing besides the iframe. With “Token only” the only addition is the token fetch on your server (step 9).
- Users can operate the simulator directly in the iframe **or** you drive it via postMessage from your page. In the sidebar they can also add a creative by **URL** (field below the upload area, HLS/DASH streams too) or drag a link from another tab onto the upload area.
- The iframe loads video footage, so a stable internet connection helps.

## Troubleshooting

### The iframe stays blank / white area

Most likely the embedding domain is not whitelisted. The browser console (F12) then shows “Refused to frame … frame-ancestors …”. Enter the exact domain (incl. https:// and any subdomain) in VirtualCampaign under Settings → Kreativsimulator, or send it to us and we'll whitelist it.

### The iframe shows no products but a notice that it is only reachable via the provider's website

The subdomain is set to “Token only” and the iframe came without a token. Check that your page embeds embedUrl (with #vcToken=…) and not the bare address.

### The iframe says the session has expired

The token had expired or was already used (e.g. the page was reloaded with the same address), or the session is over. Fetch a new token for every page view. For long sessions use accessRequired + setAccessToken (step 9).

### The iframe shows no products / “no access”

The tenant is not yet fully set up server-side (e.g. no key stored for the Kreativsimulator). Drop us a line and we'll sort it out.

### A creative doesn't appear (error event)

The message in the error event names the reason. Common: the URL needs a login, returns an HTTP error or points to an internal address – or the loader name doesn't match the active product. Open the URL in a private browser window to test it, and send loader-targeted commands only after the productChanged event.

## Ready to embed?

With access to VirtualCampaign you enter your domains yourself. Otherwise send them to us. We'll whitelist the embed and let you know as soon as everything is ready.

Contact: https://virtualcampaign.dev/en/#contact — tenants with VirtualCampaign access whitelist their domains themselves; everyone else sends them to us and we whitelist them.
