Skip to content

Embed the Creative Simulator via <iframe>

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.

For AI coding agents (Claude Code, Cursor & co.)

This guide is also available as Markdown at its own URL, which your coding agent can read directly. Give it this prompt:

Prompt Prompt
Prompt
Read https://virtualcampaign.dev/embed.md and embed the Creative Simulator into our website via iframe.

/embed.md /llms.txt

https://virtualcampaign.dev/llms.txt also links the docs following the llms.txt convention, so AI agents find them on their own.

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.

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).

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

Embed URL: https://<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 &):

ParameterValuesEffectDefault
langde · en · fr · itUI languageTenant → browser → de
productProduct code (ec5472c2)Loads this product directly (deep link). Unknown code → first product.First product
header0 · 1Show/hide the header (product selector)1 (visible)
sidebar0 · 1Show/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)
playback0 · 1Show/hide the playback/transport buttons in the bar1 (visible)
timeline0 · 1Show/hide the timeline (scrubbing bar)1 (visible)
export0 · 1Show/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)
effects0 · 1Show/hide the effect picker (photo products only, button bottom left on the image), e.g. for your own picker1 (visible)
viewView key (0e23675447)Only with the view picker enabled (see “Views”): starts the product from product in this view. Unknown key → default view.Default view
views0 · 1Only with the view picker enabled: show/hide the “Views (n)” button top left on the image, e.g. for your own picker1 (visible)
selection0 · 1Only with the selection enabled (see “Selection, share link and slideshow”): show/hide the + “Add” and “Selection” buttons1 (visible)
library0 · 1Only with the product library enabled (see “Product library”): show/hide the “Catalog (n)” button. The product bar stays.1 (visible)
hint0 · 1Tap 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)
editorauto · sidebar · overlayWhere 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
selvalue from selectionLinkCopied.param or selectionChanged.paramOnly with the selection enabled: opens a shared or saved selection (products, views and creatives by URL)–
fullscreen0 · 1Fullscreen icon: 0=never, 1=always (incl. desktop); omitted = mobile / in fullscreen onlyadaptive
fit0 · 11 = 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)
embedminimal · kioskShorthand: hides header, sidebar and export (playback bar stays)–
themeURL to a CSS fileLoads a custom theme (CSS variables) that overrides the tenant theme–
bgany CSS color, transparent or image URLBackground of the area around the canvas (letterbox/frame), not the videoTheme background
stageauto · <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
placeholderBgany CSS colorBackground of the placeholder box (loader without a creative) – direct shortcut, no ?theme= fileTheme/default (#ffffff)
placeholderTextany CSS colorColor of the letter inside the placeholder boxTheme/default (#000000)
placeholderBorderany CSS colorBorder color of the placeholder boxTheme/default (#000000)
watermark0 · 11 = 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)
logo0 · 10 = 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

View code URL
URL
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:

View code HTML
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:

View code HTML
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:

View code JavaScript
JavaScript
// 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:

View code HTML + JS
HTML + JS
<!-- 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

View code HTML
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

Download template ks-theme.css
View template kreativsimulator-theme-template.css
kreativsimulator-theme-template.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 61

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).

VariableEffectDefault
--vc-color-schemelight/dark scheme (dark · light) – affects native form elements among othersdark
--vc-font-familyfont family of the entire UI (load custom webfonts via @import at the top of the theme CSS)system-ui, …
--vc-bgapp background / canvas background while loading#0a0a0a
--vc-surfacesidebar, dialog surfaces#181818
--vc-surface-raisedraised elements (tracks, chips, inputs)#2a2a2a
--vc-surface-hoverhover surface#3a3a3a
--vc-borderdefault border color#2a2a2a
--vc-border-strongemphasized borders#444444
--vc-textdefault text color#eeeeee
--vc-text-mutedsecondary text#888888
--vc-text-subtlesubtle hints#666666
--vc-text-on-accenttext on accent/primary surfaces#ffffff
--vc-accentbrand accent: active states, in/out marks, slider handlestenant theme
--vc-accent-rgbthe same color as “R, G, B” (without rgb() parentheses) – used for translucent highlights; must match --vc-accenttenant theme
--vc-accent-strongreserved – currently read by no UI element–
--vc-accent-mutedmuted accent background#3a3a3a
--vc-dangerdelete: bin in the selection list, “Clear” tile, confirm button in delete prompts (selection only)#e5484d
--vc-warningwarning: icon for empty advertising surfaces before export and sharing (selection only)#f5a524
--vc-hint-colortap hint: placeholders pulse once after loadingfollows --vc-accent
--vc-header-bgheader backgroundtransparent
--vc-header-textheader text (active product label, menu icon)#eeeeee
--vc-header-text-mutedsecondary header text (inactive product labels)#888888
--vc-header-borderbottom divider of the header#1a1a1a
--vc-header-border-widthdivider width (0px = none)1px
--vc-header-paddingheader padding12px 20px
--vc-button-bgbutton surface#2a2a2a
--vc-button-bg-hoverbutton surface on hover#3a3a3a
--vc-button-textbutton text colorinherit
--vc-button-borderbutton border#444444
--vc-button-radiusbutton corner radius4px
--vc-button-inactive-bgsurface of the inactive selector button (placeholder/scene selector) – deliberately neutral instead of accent, so active/inactive separate visibly#3a3a3a
--vc-button-inactive-texttext color of the inactive selector button#eeeeee
--vc-slider-tracktrack color of the sliders#3a3a3a
--vc-playhead-colorplayhead (line + handle) of both timelines – main timeline + placeholder mini barfollows --vc-accent
--vc-timeline-dimveil outside the in/out marks – adjust for light themes (see note)rgba(0, 0, 0, 0.55)
--vc-timeline-dim-texttimecodes inside the veil – adjust for light themes (see note)#ffffff
--vc-canvas-outline-coloroutline color of the preview canvas#2a2a2a
--vc-canvas-outline-widthoutline width (0px = no frame)1px
--vc-canvas-radiusrounded canvas corners (video, loading area and frame alike)0px (square)
--vc-spinner-colorloading spinner: rotating arcfollows --vc-accent
--vc-spinner-trackloading spinner: ring/trackfollows --vc-border
--vc-spinner-sizeloading spinner: diameter32px
--vc-frame-coloroptional outer frame around the whole area: colortransparent
--vc-frame-widthoptional outer frame: width (0px = off)0px
--vc-placeholder-bgbackground of the placeholder box (generated “A/B” texture)#ffffff
--vc-placeholder-textletter/label of the placeholder box#000000
--vc-placeholder-borderborder of the placeholder box#000000
--vc-dialog-bgsurface of the dialogs (export, selection, catalog …)follows surface
--vc-dialog-radiuscorner radius of the dialogs (sheet from below: +4px)16px
--vc-dialog-backdropdimming behind the dialog (slightly blurred)rgba(0,0,0,0.78)
--vc-slideshow-bgbackground of the slideshow page (slideshow enabled only)follows --vc-bg
--vc-input-bginput/select surface (incl. the effect dropdown on photo products)follows surface
--vc-input-borderinput/select borderfollows border
--vc-input-texttext color in input/selectfollows --vc-text
--vc-input-radiusinput/select corner radiusfollows --vc-button-radius (4px)
--vc-input-iconchevron arrow of the dropdownfollows --vc-accent
--vc-input-focusfocus ring + hover border of input/selectfollows --vc-accent
--vc-input-selected-bgopen list: background of the selected entryfollows --vc-accent
--vc-input-selected-textopen list: text color of the selected entryfollows --vc-text-on-accent
--vc-input-option-hoveropen list: entry under the mousefollows --vc-surface-hover
--vc-label-textlabels above the fieldsfollows --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 7 · Interactive

Try remote control live

On the left you see the real iframe, on the right a custom control panel with product images, creatives and buttons, just as you would build it on your site. Every click sends a postMessage to the iframe. The code for it is right below.

Real iframe from demo.kreativsimulator.com Open in new tab

Show / hide chrome

Hide header/sidebar/buttons live → setChrome. “Canvas only” hides everything.

Your playback bar

Works even without visible buttons. Scrubber via seek, synced via playbackState.

Your product picker

Your page builds these cards from the ready event. Click → setProduct. Photo products show their effect count via getEffects (pre-fetched, no product switch).

Your creatives

Your own placeholder creatives. Click → setCreative with the image URL.

Sends a public HLS live stream (.m3u8) via setCreative. Reply in the log: creativeLoaded with live: true.

Your own file per placeholder

Drag an image onto A or B → setCreative with loader. In production you put the file on a public host and pass the URL; here it's a data URL for the demo.

Export

Resolution from ready.render.presets. Click → export (MP4).

Off = logo: false on export/exportImage – the tenant logo top-left is then missing from the MP4 or JPG.

Load variants (URL parameters)

Reloads the iframe with parameters – this is how embed/bg/stage take effect.

Placeholder colors (URL)

Colors just the placeholder box, directly via URL and without a ?theme= file. “Apply” reloads the iframe with ?placeholderBg/Text/Border.

Language

Event log (iframe → your page)

  • No events received yet …

This is the code that runs the live demo above. Feel free to use it as your starting point:

View code HTML + JS
HTML + JS
<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>
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

  • { 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

  • { 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

  • { 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

  • { 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

  • { 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)

  • { 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".

View code JS
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 });

Outgoing events (iframe → your page)

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.

View code JS
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" }

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:

View code JS
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.

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:

View code URL
URL
https://virtualcampaign-prod.nbg1.your-objectstorage.com/assets-public/<token>/web_720.webp
View code JS
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",
});
VariantSizeWhen
web_720.mp4 · web_720.webpup to 1280 × 720default – best quality
web_640.mp4 · web_640.webpup to 1138 × 640compromise
web_320.mp4 · web_320.webpup to 570 × 320slow 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.

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”.

View code JS
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
});

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:

View code HTML + JS
HTML + JS
<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 fieldRequiredMeaning
siteno*Your subdomain, e.g. "demo" for demo.kreativsimulator.com. *Required if your tenant has several subdomains.
ttlSecondsnoToken lifetime in seconds, 60 to 900. Default 300 (5 minutes).
View code curl
curl
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):

View code JSON
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):

View code Node.js
Node.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 #:

View code HTML
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:

View code JS
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

HTTPcodeMeaning
401INVALID_API_KEYKey missing, wrong or deactivated.
403KREATIVSIMULATOR_REQUIREDKey is not of type “Kreativsimulator”.
403GROUP_REQUIREDKey has no user group.
403FEATURE_DISABLEDREST API or Kreativsimulator not enabled for the tenant.
404SITE_NOT_FOUNDSubdomain unknown, inactive or not part of your tenant.
400SITE_REQUIREDTenant has several subdomains. Pass site.
429RATE_LIMITToo many requests. Respect Retry-After.
Step 10 · Optional

Selection, share link and slideshow

VirtualCampaign enables 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:

View code HTML
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

VirtualCampaign enables 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.