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.
| Layout | Parent application owns | Hosted iframe owns |
|---|---|---|
| Full page | The complete responsive page and every control around the viewer | The 3D scene, attachment-point interaction, rules, pricing and persistence |
| Modal | The product page, modal, focus/close behaviour and every control in the modal | The 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:
Both of these URLs load the same Snap2 range:
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.
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:
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 "*".
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.
The core Snap2 messages are:
| Message | Use in the parent UI |
|---|---|
COMPATIBLE_MODULES | Render the initial module picker or a later add/replace-module picker. |
CONFIGURATOR_STATE | Render options, groups, selections and the current snap2Objects. |
SELECTED_SELECTIONS | Optional lightweight update containing the selected ID triples. |
CURRENT_PRICE | Render the scene total and per-product price lines. |
CURRENT_SKU | Build the multi-line basket payload keyed by product ID. |
IS_LOADING | Show or hide the parent loading cover and disable controls. |
SNAP2_SAVE_RESPONSE | Create a restorable link after a save request. |
ERROR | Surface 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:
Render the list from the message payload and send the selected model data back unchanged:
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.
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.productBreakdownscontains one line per billable product.CURRENT_SKUis an object keyed by product ID.- monetary numbers are minor units, for example pence.
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:
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.
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:
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:
Load the iframe only after the dialog has a visible size, then keep the same iframe instance for later opens:
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:
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
| Message | Payload before serialization | Response and interaction |
|---|---|---|
REQUEST_CURRENT_PRODUCT_ID | {} | Requests CURRENT_PRODUCT_ID. |
SELECT_PRODUCT | Product ID number | Shared 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" } pair | Applies 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_CAMERA | Camera ID string | Selects 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_OVERRIDE | Camera controls override object, or null to clear | Applies camera locks and limits. Replies with SET_CAMERA_CONTROLS_OVERRIDE { success: true } or ERROR. See Camera Controls. |
SELECT_LIGHT | Light-group ID string | Selects 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:
Snap2 iframe → parent application
| Message | Parsed payload | What the parent should do |
|---|---|---|
ALL_PRODUCTS | Array of cleaned product records | Optional catalogue metadata lookup. Module placement must still use COMPATIBLE_MODULES. |
RANGE | Current range object | Store range-level names, IDs and metadata needed by the shell. |
CURRENT_PRODUCT_ID | Product ID number or null | Track the active shared configurator/variant context. Also answers REQUEST_CURRENT_PRODUCT_ID. |
SELECT_PRODUCT_RECEIVED | Echo of the requested product ID | Clear 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_SELECTIONS | Array of { optionId, groupId, selectionId } | Apply a lightweight selected-state update without re-reading the entire configuration tree. |
CURRENT_QUERY_STRING | Query-string text | Optional 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_SKU | Object keyed by product ID; each value is { skuString, skuMap, quantity } | Build the multi-line basket request and reconcile it with CURRENT_PRICE.productBreakdowns. |
IS_LOADING | Boolean | When 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_CAMERAS | Array 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 degrees | Optional 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_LIGHTS | Array of { id, displayName } | Render an optional lighting picker. |
SELECT_LIGHT | { success: boolean } | Acknowledges SELECT_LIGHT. |
AVAILABLE_FABRICS | Array of fabric objects | Render 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_LINK | URL string | Present the AR link or turn it into a QR code. |
AR_GLB_DATA | Raw base64 GLB string, not JSON | Decode 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_SNAPSHOT | Top-level { requestId, bitmap: ImageBitmap }, not payload JSON | Draw the transferable bitmap into a canvas, then call bitmap.close() when finished. |
TRANSITION_SNAPSHOT_ERROR | Top-level requestId plus JSON { message } in payload | Match 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:
User-interaction sequence lookup
| Customer or host interaction | Message sequence | Completion condition |
|---|---|---|
| Open a fresh full page or modal | Parent 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 module | COMPATIBLE_MODULES → SELECT_MODULE → SELECT_MODULE_RECEIVED → refreshed state/price/SKU. | The acknowledgement succeeds and CONFIGURATOR_STATE.snap2Objects contains the module. |
| Add or replace a module | Customer 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 picker | Parent sends CLOSE_MODULE_SELECT_MENU, then closes its own picker. | The current point/object selection is cleared; there is no acknowledgement. |
| Change a variant | Parent 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 basket | No 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 share | REQUEST_SNAP2_SAVE → SNAP2_SAVE_RESPONSE. | Store the successful UUID in the parent URL as configuration_uuid. |
| Restore a scene | No 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 modal | No 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 floor | Use the matching command above; consume its acknowledgement where one exists. | Command-specific acknowledgement, or the visible viewer change for toggle-only commands. |
| Capture imagery | CAPTURE_SCREENSHOT → SCREENSHOT_URL, or SNAP2_CAPTURE_SCREENSHOTS → SNAP2_SCREENSHOTS_RESULT. | Receive the expected response for the matching requestId, or handle ERROR. |
| Launch AR | ENTER_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.originandevent.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_STATEand honour everySHOW_*/HIDE_*update. - Disable parent controls while
IS_LOADINGis true. - Build basket lines from raw
CURRENT_SKUandCURRENT_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.