OV25

Custom Snap2 Integration

Build your own full-page or modal Snap2 interface around the hosted OV25 iframe.

This guide is for integrations that use the hosted Snap2 iframe directly and build all customer-facing controls in the parent application. It does not use ov25-ui or injectConfigurator.

OV25 owns the 3D scene, configuration rules, module compatibility, pricing, SKU calculation, and saved scene data. Your application owns the starting-module picker, compatible-module picker, variant controls, price and basket UI, loading states, and either a full-page or modal shell.

LayoutParent application ownsHosted iframe owns
Full pageThe complete responsive page and every control around the viewerThe 3D scene, attachment-point interaction, rules, pricing and persistence
ModalThe product page, modal, focus/close behaviour and every control in the modalThe same hosted Snap2 scene and data services

Both layouts use the same iframe URL and postMessage contract. Only the parent-page shell changes.

The snippets below form one framework-neutral example. Run the JavaScript after the chosen shell markup exists, or translate the same state and message flow into React, Vue or your application framework.

Before you begin

You need:

  • a Product Configurator Access API key;
  • the Snap2 range ID or range name from OV25;
  • every production and preview hostname added to the API key's authorised domains.

Snap2 uses this URL:

https://configurator.orbital.vision/[API_KEY]/snap2/[RANGE_ID_OR_NAME]

Both of these URLs load the same Snap2 range:

https://configurator.orbital.vision/15-your-public-key/snap2/693
https://configurator.orbital.vision/15-your-public-key/snap2/clerkenwell

Numeric IDs are exact and remain stable if a range is renamed. Names are case-insensitive, URL-decoded and fuzzy-matched within the API key's organisation using the same resolver as standard range URLs. URL-encode spaces and punctuation when constructing the path.

The API key appears in browser code, so use a Product Configurator Access key restricted to your authorised domains. Never place a secret server API key in the URL.

1. Create the iframe URL

Build the URL with the browser URL API. If the parent page contains a saved configuration_uuid, forward it to the iframe to restore that scene.

const CONFIGURATOR_ORIGIN = 'https://configurator.orbital.vision';
const apiKey = 'YOUR_PRODUCT_CONFIGURATOR_ACCESS_KEY';
const rangeIdOrName = 'clerkenwell'; // Or a numeric ID such as '693'.
 
function buildConfiguratorUrl() {
  const rangeSegment = encodeURIComponent(rangeIdOrName);
  const url = new URL(`${CONFIGURATOR_ORIGIN}/${apiKey}/snap2/${rangeSegment}`);
  const savedUuid = new URL(window.location.href).searchParams.get('configuration_uuid');
 
  if (savedUuid) {
    url.searchParams.set('configuration_uuid', savedUuid);
  }
 
  return url.toString();
}

For an optional parent-page spin/tilt indicator, add url.searchParams.set('reportCameraOrbit', 'true') inside buildConfiguratorUrl, before returning the URL. Listen for CAMERA_ORBIT_CHANGED; see Listening for camera movement for angle definitions and a safe listener.

Use these permissions on the iframe so optional camera, AR and fullscreen features are not blocked by the parent page:

<iframe
  id="snap2-frame"
  title="Configure your modular product in 3D"
  allow="camera; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; xr-spatial-tracking; fullscreen"
  allowfullscreen
></iframe>

Do not use the iframe's load event as the scene-ready signal. It only means the iframe document loaded. Drive the customer-facing loading state from IS_LOADING messages.

2. Create a safe message bridge

OV25 messages use { type, payload }. The payload is normally JSON serialized. Validate both the configurator origin and the exact iframe window before accepting a message, and send commands to the exact origin instead of "*".

const iframe = document.querySelector('#snap2-frame');
 
function parsePayload(payload) {
  if (typeof payload !== 'string') return payload;
  try {
    return JSON.parse(payload);
  } catch {
    // AR_GLB_DATA is a raw base64 string rather than JSON.
    return payload;
  }
}
 
function sendToSnap2(type, payload = {}) {
  iframe.contentWindow?.postMessage(
    { type, payload: JSON.stringify(payload) },
    CONFIGURATOR_ORIGIN,
  );
}
 
function onSnap2Message(event) {
  if (event.origin !== CONFIGURATOR_ORIGIN) return;
  if (event.source !== iframe.contentWindow) return;
  if (!event.data?.type) return;
 
  let data;
  try {
    data = parsePayload(event.data.payload);
  } catch (error) {
    console.error('Invalid Snap2 message', event.data, error);
    return;
  }
 
  updateSnap2State(event.data.type, data);
}
 
window.addEventListener('message', onSnap2Message);
 
// When destroying the integration:
// window.removeEventListener('message', onSnap2Message);

Use the matching local configurator origin when developing against a local OV25 environment.

3. Store the iframe state

The parent UI should be a projection of messages received from OV25. Do not duplicate module compatibility or configuration rules in the storefront.

const snap2State = {
  configuration: null,
  modules: [],
  modulesAreInitial: false,
  moduleRequestPending: false,
  price: null,
  skuByProductId: null,
  loading: true,
  error: null,
  hiddenOptions: new Set(),
  hiddenGroups: new Set(),
  hiddenSelections: new Set(),
};
 
function groupKey(optionId, groupId) {
  return `${optionId}:${groupId}`;
}
 
function selectionKey(optionId, groupId, selectionId) {
  return `${optionId}:${groupId}:${selectionId}`;
}
 
function updateVisibility(type, data) {
  const show = type.startsWith('SHOW_');
  let collection;
  let key;
 
  if (type.endsWith('_OPTION')) {
    collection = snap2State.hiddenOptions;
    key = data.optionId;
  } else if (type.endsWith('_GROUP')) {
    collection = snap2State.hiddenGroups;
    key = groupKey(data.optionId, data.groupId);
  } else {
    collection = snap2State.hiddenSelections;
    key = selectionKey(data.optionId, data.groupId, data.selectionId);
  }
 
  if (show) collection.delete(key);
  else collection.add(key);
  renderVariants();
}
 
function updateSnap2State(type, data) {
  switch (type) {
    case 'CONFIGURATOR_STATE':
      snap2State.configuration = data;
      renderVariants();
      break;
 
    case 'COMPATIBLE_MODULES':
      snap2State.modules = data.modules ?? [];
      snap2State.modulesAreInitial = Boolean(data.isInitialLoad);
      snap2State.moduleRequestPending = false;
      renderModulePicker();
      break;
 
    case 'SELECT_MODULE_RECEIVED':
      snap2State.moduleRequestPending = false;
      if (data.success) closeModulePicker();
      break;
 
    case 'CURRENT_PRICE':
      snap2State.price = data;
      renderCommerceState();
      break;
 
    case 'CURRENT_SKU':
      snap2State.skuByProductId = data;
      renderCommerceState();
      break;
 
    case 'IS_LOADING':
      snap2State.loading = Boolean(data);
      document.querySelector('#snap2-loading').hidden = !snap2State.loading;
      break;
 
    case 'SNAP2_SAVE_RESPONSE':
      handleSaveResponse(data);
      break;
 
    case 'SHOW_OPTION':
    case 'HIDE_OPTION':
    case 'SHOW_GROUP':
    case 'HIDE_GROUP':
    case 'SHOW_SELECTION':
    case 'HIDE_SELECTION':
      updateVisibility(type, data);
      break;
 
    case 'ERROR':
      snap2State.moduleRequestPending = false;
      snap2State.error = data?.message ?? 'The configurator reported an error.';
      renderModulePicker();
      showSnap2Error(snap2State.error);
      break;
  }
}
 
function showSnap2Error(message) {
  const element = document.querySelector('#snap2-error');
  if (!element) return;
  element.textContent = message;
  element.hidden = !message;
}

The core Snap2 messages are:

MessageUse in the parent UI
COMPATIBLE_MODULESRender the initial module picker or a later add/replace-module picker.
CONFIGURATOR_STATERender options, groups, selections and the current snap2Objects.
SELECTED_SELECTIONSOptional lightweight update containing the selected ID triples.
CURRENT_PRICERender the scene total and per-product price lines.
CURRENT_SKUBuild the multi-line basket payload keyed by product ID.
IS_LOADINGShow or hide the parent loading cover and disable controls.
SNAP2_SAVE_RESPONSECreate a restorable link after a save request.
ERRORSurface a retryable customer message and log diagnostic context.

4. Build the module picker

The hosted Snap2 iframe intentionally does not render its own starting-module or compatible-module menus when embedded. Your parent application must render them from COMPATIBLE_MODULES.

The payload has two modes:

  • isInitialLoad: true: no scene exists yet; ask the customer to choose a starting module.
  • isInitialLoad: false: the customer selected an attachment point or placed object inside the 3D scene; offer the compatible add/replace choices.

Add a module-picker region to your shell:

<section id="snap2-module-picker" class="module-picker" hidden>
  <header>
    <h2 id="snap2-module-title"></h2>
    <button id="snap2-close-modules" type="button">Close</button>
  </header>
  <div id="snap2-module-list" class="module-list"></div>
</section>

Render the list from the message payload and send the selected model data back unchanged:

const modulePicker = document.querySelector('#snap2-module-picker');
const moduleTitle = document.querySelector('#snap2-module-title');
const moduleList = document.querySelector('#snap2-module-list');
 
function renderModulePicker() {
  const modules = snap2State.modules;
  modulePicker.hidden = modules.length === 0;
  moduleTitle.textContent = snap2State.modulesAreInitial
    ? 'Choose a starting module'
    : 'Choose a compatible module';
 
  const buttons = modules.map((module) => {
    const button = document.createElement('button');
    button.type = 'button';
    button.className = 'module-card';
    button.disabled = snap2State.moduleRequestPending || !module.model?.modelPath;
 
    const imageUrl = module.product?.imageUrls?.thumbnail ?? module.product?.imageUrl;
    if (imageUrl) {
      const image = document.createElement('img');
      image.src = imageUrl;
      image.alt = '';
      button.append(image);
    }
 
    const name = document.createElement('span');
    name.textContent = module.product?.name ?? 'Module';
    button.append(name);
 
    button.addEventListener('click', () => {
      snap2State.moduleRequestPending = true;
      renderModulePicker();
      sendToSnap2('SELECT_MODULE', {
        modelPath: module.model.modelPath,
        modelId: module.model.modelId,
      });
    });
 
    return button;
  });
 
  moduleList.replaceChildren(...buttons);
}
 
function closeModulePicker() {
  modulePicker.hidden = true;
  sendToSnap2('CLOSE_MODULE_SELECT_MENU');
}
 
document
  .querySelector('#snap2-close-modules')
  .addEventListener('click', closeModulePicker);

Each module can also contain dimensions, position, descriptions and several image sizes. Use those fields for richer cards, but keep model.modelPath and model.modelId as the command identifiers.

After the first module is placed, the customer chooses an attachment point or object in the iframe. OV25 calculates compatibility and emits a new COMPATIBLE_MODULES payload; the parent should reopen this picker whenever that array is non-empty.

5. Build the variant controls

CONFIGURATOR_STATE.options contains options with nested groups and selections. Send the selected ID triple back with SELECT_SELECTION.

<div id="snap2-options" class="snap2-options"></div>
function isSelected(optionId, groupId, selectionId) {
  return snap2State.configuration?.selectedSelections?.some(
    (selected) =>
      selected.optionId === optionId &&
      selected.groupId === groupId &&
      selected.selectionId === selectionId,
  );
}
 
function renderVariants() {
  const container = document.querySelector('#snap2-options');
  const options = snap2State.configuration?.options ?? [];
  const fields = [];
 
  for (const option of options) {
    if (snap2State.hiddenOptions.has(option.id)) continue;
 
    const fieldset = document.createElement('fieldset');
    const legend = document.createElement('legend');
    legend.textContent = option.name;
    fieldset.append(legend);
 
    for (const group of option.groups ?? []) {
      if (snap2State.hiddenGroups.has(groupKey(option.id, group.id))) continue;
 
      for (const selection of group.selections ?? []) {
        if (
          snap2State.hiddenSelections.has(
            selectionKey(option.id, group.id, selection.id),
          )
        ) continue;
 
        const button = document.createElement('button');
        button.type = 'button';
        button.textContent = selection.name;
        button.disabled = snap2State.loading;
        button.setAttribute(
          'aria-pressed',
          String(isSelected(option.id, group.id, selection.id)),
        );
 
        button.addEventListener('click', () => {
          sendToSnap2('SELECT_SELECTION', {
            optionId: option.id,
            groupId: group.id,
            selectionId: selection.id,
          });
        });
 
        fieldset.append(button);
      }
    }
 
    fields.push(fieldset);
  }
 
  container.replaceChildren(...fields);
}

The ID triple is the most deterministic integration. A single name pair such as { "Fabric": "Natural Linen" } is also accepted, but it relies on fuzzy display-name matching.

The SHOW_* and HIDE_* messages in the state example keep the parent controls aligned with OV25 configuration rules. Do not let a customer select a hidden option, group or selection.

6. Render price, SKU and basket lines

Raw Snap2 payloads differ from the normalized payloads produced by the OV25 UI package:

  • CURRENT_PRICE.productBreakdowns contains one line per billable product.
  • CURRENT_SKU is an object keyed by product ID.
  • monetary numbers are minor units, for example pence.
<div id="snap2-total" aria-live="polite"></div>
<button id="snap2-add-to-basket" type="button">Add configuration to basket</button>
function renderCommerceState() {
  document.querySelector('#snap2-total').textContent =
    snap2State.price?.formattedPrice ?? 'Price loading';
}
 
function buildSnap2CartLines() {
  if (!snap2State.price || !snap2State.skuByProductId) return [];
 
  const pricesByProductId = new Map(
    snap2State.price.productBreakdowns.map((line) => [String(line.productId), line]),
  );
 
  return Object.entries(snap2State.skuByProductId).map(([productId, sku]) => {
    const price = pricesByProductId.get(productId);
    return {
      productId,
      sku: sku.skuString,
      skuMap: sku.skuMap,
      quantity: sku.quantity,
      unitPrice: price ? price.price / price.quantity : undefined,
      linePrice: price?.price,
    };
  });
}
 
document.querySelector('#snap2-add-to-basket').addEventListener('click', () => {
  if (snap2State.loading) return;
 
  const lines = buildSnap2CartLines();
  if (lines.length === 0) return;
 
  // Send these lines plus snap2State.price.totalPrice to your backend.
  console.log(lines, snap2State.price.totalPrice);
});

Treat iframe prices as display and configuration data. Your commerce backend must validate product IDs, SKUs, quantities, prices, discounts, tax and inventory before creating an order.

7. Save and restore scenes

Request a server-side Snap2 save and wait for SNAP2_SAVE_RESPONSE:

<button id="snap2-save" type="button">Save configuration</button>
<input id="snap2-share-url" type="url" readonly aria-label="Saved configuration link" />
document.querySelector('#snap2-save').addEventListener('click', () => {
  sendToSnap2('REQUEST_SNAP2_SAVE');
});
 
function handleSaveResponse(data) {
  if (!data.success || !data.uuid) {
    showSnap2Error(data.error ?? 'Could not save this configuration.');
    return;
  }
 
  const shareUrl = new URL(window.location.href);
  shareUrl.searchParams.set('configuration_uuid', data.uuid);
  window.history.replaceState(window.history.state, '', shareUrl);
  document.querySelector('#snap2-share-url').value = shareUrl.toString();
}

On the next visit, buildConfiguratorUrl() forwards the UUID to the hosted iframe. While a saved scene loads, the iframe does not send the fresh-scene starting modules; wait for CONFIGURATOR_STATE.snap2Objects and IS_LOADING: false instead.

8. Full-page shell

The full-page version keeps one iframe mounted and gives the viewer and parent controls bounded areas inside the viewport.

<main class="snap2-shell">
  <section class="snap2-viewer" aria-label="3D configurator">
    <iframe
      id="snap2-frame"
      title="Configure your modular product in 3D"
      allow="camera; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; xr-spatial-tracking; fullscreen"
      allowfullscreen
    ></iframe>
    <div id="snap2-loading" class="snap2-loading" role="status">
      Loading 3D configuration…
    </div>
  </section>
 
  <aside class="snap2-sidebar" aria-label="Configuration controls">
    <div id="snap2-total" aria-live="polite"></div>
    <div id="snap2-options" class="snap2-options"></div>
    <button id="snap2-add-to-basket" type="button">Add configuration to basket</button>
    <button id="snap2-save" type="button">Save configuration</button>
    <input id="snap2-share-url" type="url" readonly aria-label="Saved configuration link" />
 
    <section id="snap2-module-picker" class="module-picker" hidden>
      <header>
        <h2 id="snap2-module-title"></h2>
        <button id="snap2-close-modules" type="button">Close</button>
      </header>
      <div id="snap2-module-list" class="module-list"></div>
    </section>
 
    <p id="snap2-error" role="alert" hidden></p>
  </aside>
</main>
html,
body,
#app {
  width: 100%;
  height: 100%;
  margin: 0;
}
 
body {
  overflow: hidden;
}
 
.snap2-shell {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(320px, 420px);
  width: 100%;
  height: 100dvh;
  min-height: 0;
  overflow: hidden;
}
 
.snap2-viewer {
  position: relative;
  min-width: 0;
  min-height: 0;
  background: #f5f5f5;
}
 
#snap2-frame {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}
 
.snap2-loading {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  background: rgb(255 255 255 / 72%);
  pointer-events: none;
}
 
.snap2-sidebar {
  position: relative;
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  border-left: 1px solid #e5e5e5;
}
 
.snap2-options {
  flex: 1;
  min-height: 0;
  overflow: auto;
}
 
.module-picker {
  position: absolute;
  inset: 0;
  overflow: auto;
  background: white;
}
 
.module-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: 12px;
}
 
.module-card img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: contain;
}
 
@media (max-width: 767px) {
  .snap2-shell {
    grid-template-columns: 1fr;
    grid-template-rows: minmax(280px, 50dvh) minmax(0, 1fr);
  }
 
  .snap2-sidebar {
    border-top: 1px solid #e5e5e5;
    border-left: 0;
  }
}

The bridge in section 2 registers the message listener. Only after that setup is complete, assign the iframe URL so the parent cannot miss the initial module payload:

iframe.src = buildConfiguratorUrl();

9. Modal shell

The message bridge, state, module picker, variant UI and basket code are identical in a modal. Put the same .snap2-shell markup inside an accessible modal instead of making it the page root.

Reuse the shell, viewer, sidebar and module styles, but omit the full-page html, body, #app and body { overflow: hidden } rules so the product page can continue to scroll normally behind the closed modal.

A native <dialog> supplies focus trapping, Escape handling and modal semantics:

<button id="open-snap2" type="button">Configure in 3D</button>
 
<dialog id="snap2-modal" aria-labelledby="snap2-modal-title">
  <header class="snap2-modal-header">
    <h2 id="snap2-modal-title">Configure your modular product</h2>
    <button id="close-snap2" type="button" aria-label="Close configurator">×</button>
  </header>
 
  <!-- Place the complete .snap2-shell markup from the full-page example here. -->
</dialog>
#snap2-modal {
  width: min(96vw, 1600px);
  height: min(92dvh, 1000px);
  max-width: none;
  max-height: none;
  padding: 0;
  border: 0;
  border-radius: 20px;
  overflow: hidden;
}
 
#snap2-modal::backdrop {
  background: rgb(0 0 0 / 55%);
}
 
#snap2-modal .snap2-shell {
  height: calc(100% - 64px);
}
 
.snap2-modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 64px;
  padding: 0 20px;
}

Load the iframe only after the dialog has a visible size, then keep the same iframe instance for later opens:

const modal = document.querySelector('#snap2-modal');
let iframeStarted = false;
 
document.querySelector('#open-snap2').addEventListener('click', () => {
  modal.showModal();
 
  if (!iframeStarted) {
    iframeStarted = true;
    iframe.src = buildConfiguratorUrl();
  }
});
 
document.querySelector('#close-snap2').addEventListener('click', () => {
  modal.close();
});
 
modal.addEventListener('close', () => {
  closeModulePicker();
});

Do not replace the iframe every time the modal opens. Keeping one instance preserves the WebGL scene and the customer's in-progress configuration. If closing the modal should warn about unsaved work, check snap2State.configuration?.snap2Objects?.length and show your confirmation UI before calling modal.close().

Complete postMessage lookup

The tables below cover every message supported by the hosted Snap2 iframe, including shared configurator features such as cameras, lights, AR, analytics and screenshots. CURRENT_BED_SIZE is intentionally excluded because it is only emitted by the separate bed configurator.

Except for the two transferable snapshot messages noted below, the envelope is always:

type Snap2Message = {
  type: string;
  payload: string; // normally JSON.stringify(value)
};

Use the sendToSnap2 and parsePayload helpers from step 2. Even a primitive payload such as a camera ID must be JSON serialized before it is sent.

Parent application → Snap2 iframe

MessagePayload before serializationResponse and interaction
REQUEST_CURRENT_PRODUCT_ID{}Requests CURRENT_PRODUCT_ID.
SELECT_PRODUCTProduct ID numberShared product-context switch; replies with SELECT_PRODUCT_RECEIVED and refreshed state. Do not use this to place a Snap2 module-use SELECT_MODULE.
SELECT_SELECTION{ optionId, groupId, selectionId } or one { "Option name": "Selection name" } pairApplies a variant choice. Expect refreshed selection, state, visibility, price and SKU messages; invalid name matching produces ERROR.
SELECT_MODULE{ modelPath, modelId, placeMovable?, customDimensions?: { x?, y?, z? } } or { productName, placeMovable?, customDimensions? }Adds the first module, adds at an attachment point, or replaces the selected object. Replies with SELECT_MODULE_RECEIVED or ERROR. Values from COMPATIBLE_MODULES are the safest input.
CLOSE_MODULE_SELECT_MENU{}Clears the selected attachment point or object when the parent closes its module picker. No acknowledgement.
REQUEST_SNAP2_SAVE{}Persists the scene. Replies with SNAP2_SAVE_RESPONSE.
VIEW_DIMENSIONS{ dimensions: boolean, styles?: object }Toggles the overall dimension overlay. In Snap2 this behaves as a toggle; no acknowledgement.
VIEW_MINI_DIMENSIONS{ dimensions: boolean, styles?: object }Toggles per-module dimensions. No acknowledgement.
TOGGLE_HIDE_ALL{}Hides or reveals all placed modules. No acknowledgement.
TOGGLE_SNAP2_SHOW_FLOOR{}Shows or hides the floor. No acknowledgement.
SNAP2_SWITCH_VIEW_GROUP{ groupId: number }Switches to a configured Snap2 camera/view group. Invalid input produces ERROR; success has no acknowledgement.
SNAP2_CAPTURE_SCREENSHOTS{ requestId?: string }Captures the main view and configured Snap2 camera views. Replies with SNAP2_SCREENSHOTS_RESULT or ERROR.
CAPTURE_SCREENSHOT{}Captures and uploads the current viewport. Replies with SCREENSHOT_URL or ERROR.
SELECT_CAMERACamera ID stringSelects an ID advertised by AVAILABLE_CAMERAS. Replies with SELECT_CAMERA { success: true } or ERROR.
RECENTER_CAMERA{}Recenters the active camera. Replies with CAMERA_RECENTERED.
SET_CAMERA_CONTROLS_OVERRIDECamera controls override object, or null to clearApplies camera locks and limits. Replies with SET_CAMERA_CONTROLS_OVERRIDE { success: true } or ERROR. See Camera Controls.
SELECT_LIGHTLight-group ID stringSelects an ID advertised by AVAILABLE_LIGHTS. Replies with SELECT_LIGHT { success: boolean } or ERROR.
SET_FABRIC_OVERRIDE{ materialName: string, textureId: number }Applies an item from AVAILABLE_FABRICS to a replaceable material. No acknowledgement.
ENTER_AR{} or { userAgentType: "ios" | "android" }Starts the supported AR flow. Depending on the device, the iframe can emit AR_PREVIEW_LINK or AR_GLB_DATA.
TOGGLE_ANIMATION{}Cycles each model animation between stop/loop or open/close. Observe ANIMATION_STATE.
REQUEST_TRANSITION_SNAPSHOT{ requestId: string }Requests a low-latency ImageBitmap. Replies with TRANSITION_SNAPSHOT or TRANSITION_SNAPSHOT_ERROR.

For example:

sendToSnap2('TOGGLE_SNAP2_SHOW_FLOOR');
sendToSnap2('RECENTER_CAMERA');
sendToSnap2('SELECT_CAMERA', 'front');
sendToSnap2('SNAP2_SWITCH_VIEW_GROUP', { groupId: 2 });
sendToSnap2('SNAP2_CAPTURE_SCREENSHOTS', { requestId: crypto.randomUUID() });

Snap2 iframe → parent application

MessageParsed payloadWhat the parent should do
ALL_PRODUCTSArray of cleaned product recordsOptional catalogue metadata lookup. Module placement must still use COMPATIBLE_MODULES.
RANGECurrent range objectStore range-level names, IDs and metadata needed by the shell.
CURRENT_PRODUCT_IDProduct ID number or nullTrack the active shared configurator/variant context. Also answers REQUEST_CURRENT_PRODUCT_ID.
SELECT_PRODUCT_RECEIVEDEcho of the requested product IDClear any product-switch pending state. It does not acknowledge module placement.
CONFIGURATOR_STATE{ options, selectedSelections, snap2Objects, ... }Render the variant UI and track the placed scene objects. Options contain nested groups and selections.
SELECTED_SELECTIONSArray of { optionId, groupId, selectionId }Apply a lightweight selected-state update without re-reading the entire configuration tree.
CURRENT_QUERY_STRINGQuery-string textOptional shared product/selection URL state. Restoring a saved Snap2 scene still uses configuration_uuid.
COMPATIBLE_MODULES{ modules, isInitialLoad }Open the starting picker when isInitialLoad is true; otherwise open the add/replace picker for the selected point or object. An empty array means there is nothing to show.
SELECT_MODULE_RECEIVED{ success, modelPath, modelId }Clear module-request pending state and close the picker after success.
CURRENT_PRICE{ formattedPrice, totalPrice, productBreakdowns, subtotal, formattedSubtotal, discount }Render the scene total and product/quantity price lines. Monetary number fields are in the organisation's configured minor-unit convention; use the formatted fields for display.
CURRENT_SKUObject keyed by product ID; each value is { skuString, skuMap, quantity }Build the multi-line basket request and reconcile it with CURRENT_PRICE.productBreakdowns.
IS_LOADINGBooleanWhen emitted, show the parent loading cover and disable commands while true. Also keep your own pending flags for commands that have an explicit acknowledgement.
SHOW_OPTION / HIDE_OPTION{ optionId }Update option visibility after a configurator rule runs.
SHOW_GROUP / HIDE_GROUP{ optionId, groupId }Update group visibility after a configurator rule runs.
SHOW_SELECTION / HIDE_SELECTION{ optionId, groupId, selectionId }Update selection visibility after a configurator rule runs.
AVAILABLE_CAMERASArray of { id, displayName }Render an optional camera picker.
SELECT_CAMERA{ success: true }Acknowledges SELECT_CAMERA; clear its pending state.
CAMERA_RECENTERED{ success: true }Acknowledges RECENTER_CAMERA.
CAMERA_ORBIT_CHANGED{ azimuthDegrees, polarDegrees } in degreesOptional spin/tilt indicator updates when the iframe URL includes reportCameraOrbit=true. Keep a static fallback until an update arrives.
SET_CAMERA_CONTROLS_OVERRIDE{ success: true }Acknowledges the camera-controls override.
AVAILABLE_LIGHTSArray of { id, displayName }Render an optional lighting picker.
SELECT_LIGHT{ success: boolean }Acknowledges SELECT_LIGHT.
AVAILABLE_FABRICSArray of fabric objectsRender optional fabric overrides; pair these with fabricReplaceableOptionIds and optionIdToMaterialName from CONFIGURATOR_STATE.
ANIMATION_STATE"unavailable", "stop", "loop", "open" or "close"Set the animation control's label and disabled state.
AR_PREVIEW_LINKURL stringPresent the AR link or turn it into a QR code.
AR_GLB_DATARaw base64 GLB string, not JSONDecode or forward the GLB for the Android/custom AR flow. The step 2 parser deliberately falls back to the raw string.
SCREENSHOT_URL{ url, cdnUrl? }Use the uploaded viewport image returned by CAPTURE_SCREENSHOT.
SNAP2_SCREENSHOTS_RESULT{ items: [{ label, dataUrl }], requestId }Match the response by requestId; each item is a labelled data URL.
SNAP2_SAVE_RESPONSE{ success, uuid?, error? }On success, put configuration_uuid=<uuid> in the parent URL; on failure, keep the scene and show a retry action.
TRANSITION_SNAPSHOTTop-level { requestId, bitmap: ImageBitmap }, not payload JSONDraw the transferable bitmap into a canvas, then call bitmap.close() when finished.
TRANSITION_SNAPSHOT_ERRORTop-level requestId plus JSON { message } in payloadMatch the failed request and remove the transition fallback.
ANALYTICS_EVENT{ event_name, engagement_type, _id, ...dimensions }Optional: deduplicate on _id and forward the event through your consent-aware GA4/GTM setup. See Analytics.
ERROR{ message, ...context }Clear the relevant pending state, show a retryable customer message and log the remaining context.

TRANSITION_SNAPSHOT is the only successful response whose useful data lives outside payload. Handle it before calling updateSnap2State:

if (event.data?.type === 'TRANSITION_SNAPSHOT') {
  const { requestId, bitmap } = event.data;
  drawTransitionFrame(requestId, bitmap);
  return;
}

User-interaction sequence lookup

Customer or host interactionMessage sequenceCompletion condition
Open a fresh full page or modalParent registers the listener, then sets iframe.src; iframe emits initial range, product, state, capabilities, price/SKU and COMPATIBLE_MODULES messages.COMPATIBLE_MODULES.isInitialLoad === true opens the required starting-module picker.
Choose the starting moduleCOMPATIBLE_MODULESSELECT_MODULESELECT_MODULE_RECEIVED → refreshed state/price/SKU.The acknowledgement succeeds and CONFIGURATOR_STATE.snap2Objects contains the module.
Add or replace a moduleCustomer selects a point/object inside the iframe → COMPATIBLE_MODULES.isInitialLoad === false → parent sends SELECT_MODULE.Successful acknowledgement plus refreshed objects, price and SKU.
Dismiss the module pickerParent sends CLOSE_MODULE_SELECT_MENU, then closes its own picker.The current point/object selection is cleared; there is no acknowledgement.
Change a variantParent sends SELECT_SELECTION; iframe emits selected state, visibility diffs, price and SKU updates.The chosen triple appears in SELECTED_SELECTIONS or CONFIGURATOR_STATE.selectedSelections, and commerce data has refreshed.
Add the scene to basketNo command is sent to the iframe. The parent uses the latest CURRENT_SKU and CURRENT_PRICE.productBreakdowns.All product IDs and quantities reconcile and no module/selection request is pending; validate again on the server.
Save or shareREQUEST_SNAP2_SAVESNAP2_SAVE_RESPONSE.Store the successful UUID in the parent URL as configuration_uuid.
Restore a sceneNo restore command. Create the iframe with ?configuration_uuid=<uuid>.Restored objects arrive in CONFIGURATOR_STATE.snap2Objects, followed by current price and SKU data.
Open or close the host modalNo iframe command is required. Keep the same iframe mounted; optionally send CLOSE_MODULE_SELECT_MENU when closing an open module picker.Parent focus, scroll lock and dialog state are settled without destroying the WebGL scene.
Change camera, light, dimensions or floorUse the matching command above; consume its acknowledgement where one exists.Command-specific acknowledgement, or the visible viewer change for toggle-only commands.
Capture imageryCAPTURE_SCREENSHOTSCREENSHOT_URL, or SNAP2_CAPTURE_SCREENSHOTSSNAP2_SCREENSHOTS_RESULT.Receive the expected response for the matching requestId, or handle ERROR.
Launch ARENTER_AR → device-specific AR_PREVIEW_LINK or AR_GLB_DATA.Hand the link/data to the host's AR presentation flow and clear any host pending state.

Production checklist

  • Use /[API_KEY]/snap2/[RANGE_ID_OR_NAME] for the hosted iframe; prefer the numeric ID when links must survive range renames.
  • Register the message listener before setting iframe.src.
  • Validate both event.origin and event.source.
  • Send messages to the exact configurator origin, never "*" in production.
  • Build the starting and compatible-module pickers from COMPATIBLE_MODULES.
  • Build options from CONFIGURATOR_STATE and honour every SHOW_* / HIDE_* update.
  • Disable parent controls while IS_LOADING is true.
  • Build basket lines from raw CURRENT_SKU and CURRENT_PRICE.productBreakdowns.
  • Validate all commerce data on the server.
  • Preserve one iframe instance during modal open/close cycles.
  • Test fresh scenes, saved scenes, module replacement, mobile layout, keyboard navigation, errors and retries.

See the API / Custom Integration reference for the complete message payloads and less common controls.