Viewer postMessage API
Use the viewer postMessage API when a parent HTML page embeds the Voluma viewer in an iframe and needs to coordinate page UI with viewer state. The parent can focus a marker inside the viewer, and the viewer can notify the parent when it is ready, when marker focus changes, or when a marker action sends a custom message.
Setup
Embed the custom viewer URL from Custom Viewer (Embed), then open Project Settings and set Allowed origins under Viewer API. This project-level field is saved as settings.embed.allowedParentOrigins and must contain the exact origin of the parent page.
Enter origins as comma-separated full URLs, for example https://www.example.com, https://partner.example.com. Local development origins such as http://127.0.0.1:5172 can be added while testing.
<iframe
id="voluma-viewer"
src="https://voluma.ai/embed/client/project/scene"
width="100%"
height="640"
allowfullscreen
></iframe>All runtime checks use exact origins. Parent-to-viewer commands from other origins are rejected, and viewer-to-parent messages are sent with exact targetOrigin values. Do not use * as the target origin in production parent code.
Parent to viewer
The supported parent-to-viewer command is voluma.viewer.focusMarker.
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Must be voluma.viewer.focusMarker. |
version | number | Yes | Must be 1. |
requestId | string | Yes | Parent-generated ID used to match the acknowledgement or error. |
markerId | string or number | Yes | Marker ID to focus. |
const iframe = document.querySelector("#voluma-viewer");
const viewerOrigin = new URL(iframe.src).origin;
let requestCounter = 0;
function focusMarker(markerId) {
iframe.contentWindow?.postMessage(
{
type: "voluma.viewer.focusMarker",
version: 1,
requestId: `marker-${Date.now()}-${++requestCounter}`,
markerId
},
viewerOrigin
);
}Viewer to parent
The viewer emits typed protocol events for embed lifecycle and command feedback.
| Event type | Payload | When it is sent |
|---|---|---|
voluma.viewer.ready | { type, version } | The embed viewer is loaded and ready to receive commands. |
voluma.viewer.markerFocused | { type, version, markerId, requestId? } | A marker becomes focused. requestId is included when the event acknowledges a command. |
voluma.viewer.error | { type, version, code, message, requestId? } | A command is invalid, not allowed, unsupported, or cannot be completed. |
Known error codes are INVALID_MESSAGE, UNAUTHORIZED_ORIGIN, MARKER_NOT_FOUND, VIEWER_NOT_READY, UNSUPPORTED_VERSION, UNSUPPORTED_COMMAND, and INTERNAL_ERROR.
window.addEventListener("message", (event) => {
if (event.origin !== viewerOrigin) return;
if (!event.data || typeof event.data !== "object") return;
if (event.data.type === "voluma.viewer.ready") {
console.log("Viewer ready");
}
if (event.data.type === "voluma.viewer.markerFocused") {
console.log("Focused marker", event.data.markerId, event.data.requestId);
}
if (event.data.type === "voluma.viewer.error") {
console.warn("Viewer command failed", event.data.code, event.data.message);
}
});Marker action messages
Markers can also send custom messages to the parent page. In Studio, configure a marker action with action type postMessage, a message type, and an optional payload template.
The viewer sends marker action messages with this shape:
{
source: "volumaviewer",
type: "marker-action",
payload: {
id: 7,
parent_id: 0,
type: "sphere",
title: "Marker title"
},
timestamp: 1710000000000
}Payload templates can use {id}, {parent_id}, {type}, and {title}. If the rendered payload is valid JSON, the parent receives parsed JSON. If not, the parent receives the rendered string.
window.addEventListener("message", (event) => {
if (event.origin !== viewerOrigin) return;
const data = event.data;
if (!data || typeof data !== "object") return;
if (data.source === "volumaviewer" && data.type === "marker-action") {
console.log("Marker action payload", data.payload);
}
});Parent page pattern
For page UI that controls the iframe, keep the viewer origin in one place, validate every incoming message, and generate unique request IDs for commands. The Veerse Toren demo uses this pattern to focus viewer markers from external callout buttons, then reacts to a custom marker message by showing page content below the iframe.
const iframe = document.querySelector(".embedCode iframe");
const viewerOrigin = new URL(iframe.src).origin;
let latestRequestId = 0;
window.addEventListener("message", (event) => {
if (event.origin !== viewerOrigin) return;
const data = event.data;
if (!data || typeof data !== "object") return;
if (data.source === "volumaviewer" && data.payload?.action === "geschiedenis") {
document.querySelector(".history-section")?.classList.add("is-visible");
}
});
document.querySelectorAll("[data-marker-id]").forEach((button) => {
button.addEventListener("click", () => {
iframe.contentWindow?.postMessage(
{
type: "voluma.viewer.focusMarker",
version: 1,
requestId: `parent-${Date.now()}-${++latestRequestId}`,
markerId: button.dataset.markerId
},
viewerOrigin
);
});
});URL fallback
If the iframe is not ready, has navigated to another scene, or must be reset before focusing a marker, update the iframe URL with the marker query parameter instead of sending a command:
function buildMarkerUrl(originalIframeUrl, markerId) {
const url = new URL(originalIframeUrl, window.location.href);
url.searchParams.set("m", markerId);
return url.toString();
}
iframe.src = buildMarkerUrl(iframe.dataset.iframe || iframe.src, markerId);Use this fallback for full iframe reloads. Use voluma.viewer.focusMarker when the current iframe is loaded and should stay on the same scene.
