Start with the minimal hybrid integration or the complete external-tray recipe. The method tables below cover arguments, defaults, return values and lifecycle requirements.
General embedding, media, controls, languages, sharing and reflection of an existing viewer summary need no Commerce Engine token. Project access restrictions still apply. Authorized native prices, stock, markets and checkout are covered in Built-In Commerce Engine Integration.
Before You Start
Open the project’s Integration section in the Presenter3D dashboard to generate its embed snippet and obtain its integration reference. This section is available to every customer with a Studio-created experience, without a paid subscription.
Use the reference’s descriptions to identify the actions and configurable choices your page needs:
| Identifier | Used for |
|---|---|
| Event ID and description | The intended scene action. A configuration-opening action's optional configurationFlowId identifies the flow it opens. |
| Flow ID and branch IDs | Read trayBranches[flowId] to exclude entire branches from the injected tray. Individual nodes cannot be excluded. The tray retains the project's configuration rules; do not rebuild them on the merchant page. |
| Node/entry IDs, when separately supplied | Optional thumbnail overrides or a specific node's prerequisites for an external preset. There is no structured node catalog in getIntegrationInfo(); obtain these IDs from explicit Studio descriptions, public customer notes or Studio directly. Ordinary group-bound finish buttons do not need them. |
| Variation ID | Registered finish/media selection. |
| Preset ID and family | Explicit selection and thumbnail keys such as object:vo_01. Where an API accepts separate fields, pass target: "object" and presetId: "vo_01", not the combined key. |
| Slot and relational-group IDs | Selecting a preset at a particular location/group. |
| Supported languages | Distinguish native UI translations from full experience translations; see Languages. |
| PDF availability | Whether the experience has an authored configuration document that your page can generate. |
Instance IDs are page-defined, not Studio-issued. data-p3d-instance-id identifies a placement such as main-experience. It is optional and generated when omitted. Supply stable, distinct values when targeting particular placements; project-id identifies the experience itself.
Project IDs and configuration identifiers are not labels or SKUs. Preserve the generated poster URL/framing, provide any additional gallery images, and match the initial finish to the registered media. Check the project’s password/origin restrictions before testing. 3D requires WebGL 2; iOS 15+ is supported, subject to device resources/browser settings.
Developers and AI agents without dashboard access can retrieve the integration reference using getIntegrationInfo(). Obtain its dedicated password from the customer when protection is enabled; do not request dashboard credentials.
API Names
api below means window.presenter3d.commerce; viewer is the matching <presenter-3d> element. This historical namespace also contains non-commerce embedding APIs. Do not invent presenter3d.experience.
Wait for customElements.whenDefined("presenter-3d") and mount the target DOM before binding. API availability is not scene readiness. Use typeof viewer.callEvent === "function" for methods installed during initialization.
Read the Integration Reference
api.getIntegrationInfo({ projectId, password? }) retrieves the project’s integration reference as JSON. It requires the browser API to be available, but does not require a mounted or ready 3D viewer, WebGL, a dashboard login, or Commerce Engine registration.
| Argument / result | Contract |
|---|---|
projectId |
Required published project ID; not inferred from an embedded viewer. A call with no arguments is not supported. |
password |
Optional string. Required while the project’s integration-info password protection is enabled; omit it only when the customer has removed that protection. |
| Return value | Promise resolving to IntegrationInfoResponse, illustrated below. This is a curated integration reference, not a live configuration, authorization decision or business-data summary. |
Viewer-free developer page: load the browser API first using the project's generated loader. The minimal loader markup is below; replace YOUR_PROJECT_ID with the same ID used in the request. No <presenter-3d> element or viewer.start() is needed. If the page already loads Presenter3D, reuse that loader instead of adding another one.
<script
src="https://runner.presenter3d.com/loader.js"
data-project-id="YOUR_PROJECT_ID"
async
></script>
Run the following in a <script type="module"> on that private developer page, or in a client-side module after loading the script above. whenDefined() waits for registration; it does not download the API. Preserve a supplied network-aware bootstrap instead when applicable; loading gates can defer API availability. If registration never arrives, inspect loader/network/CSP failures rather than creating a dummy viewer.
await customElements.whenDefined("presenter-3d");
const api = window.presenter3d.commerce;
// Developer tooling only: do not publish this password in storefront code.
try {
const info = await api.getIntegrationInfo({
projectId: "YOUR_PROJECT_ID",
password: "YOUR_INTEGRATION_INFO_PASSWORD",
});
console.log(info);
} catch {
console.error("Integration information could not be retrieved.");
}
When protection has been removed, call await api.getIntegrationInfo({ projectId }). Handle request rejection; a failed request does not mean that the project has no configurable choices.
Studio initially supplies an automatically generated password dedicated to this method. The customer can change or remove it in the dashboard. It is separate from the password used to open the experience and from commerce tokens. Keep it out of published HTML/JavaScript, URLs, logs and analytics.
Studio descriptions and customer notes are separate. A new Studio description replaces the previous Studio description without replacing customer notes. The server removes notes whose API-exposure checkbox is unchecked. Passwords, allowed-origin lists and private administrative settings are not returned.
Reading this reference does not authorize loading the experience or bypass its access restrictions. For a new development origin, ask the customer to add it to the project’s permitted origins. Obtain any experience access password separately through a private channel.
Response Example
Abbreviated bar catalog, using its authored event/preset/group/slot/branch IDs. The revision, experience-language list and customer note illustrate a response; read the returned values for your published project.
{
"projectId": "bar",
"sourceVersion": 132,
"integrationInfo": {
"schemaVersion": 1,
"title": "Cabinet demonstration integration reference",
"events": {
"1790724526599": {
"kind": "action",
"label": "Setup the cabinet",
"description": "Open the cabinet configuration tray.",
"configurationFlowId": "conf"
},
"1790629112958": {
"kind": "action",
"label": "Open doors",
"description": "Open the cabinet doors."
},
"1790630976545": {
"kind": "action",
"label": "Close doors",
"description": "Close the cabinet doors."
}
},
"presets": {
"color:vc_01": {
"label": "Dark Walnut",
"description": "Apply the Dark Walnut finish.",
"bindings": [{ "relationalId": "cs_01" }]
},
"object:vo_7": {
"label": "Individual shelf",
"description": "Add or remove an individual shelf.",
"bindings": [
{ "relationalId": "os_05", "slotId": "SaA", "label": "Shelf 4" },
{ "relationalId": "os_05", "slotId": "SaB", "label": "Shelf 5" }
]
}
},
"trayBranches": {
"conf": {
"finishes": {
"label": "Finishes",
"description": "Cabinet finishes."
}
}
}
},
"capabilities": {
"languages": {
"nativeUI": ["ar", "en", "es", "fr", "he", "hi", "nl", "ru", "uk", "zh"],
"experience": ["en", "fr"]
},
"pdf": true
},
"customerNotes": {
"events": { "1790629112958": "Use this action for the product-details button." },
"presets": {},
"trayBranches": {}
},
"warnings": []
}
| Response field | Meaning |
|---|---|
projectId, sourceVersion |
Published project and source revision used for this response. Not a shared-configuration hash. |
integrationInfo.schemaVersion |
Catalog format version, currently 1; distinct from the project revision. |
integrationInfo.title |
Catalog title. There is no catalog-level language or project-description field. |
integrationInfo.events |
Real event IDs mapped to { kind, label?, description } plus kind-specific fields. kind is "action" or "liveControl". Only live controls can have measurement: { unit, atPhase0, atPhase1, description }, mapping a normalized phase to an authored linear measurement. |
integrationInfo.events[eventId].configurationFlowId |
Optional string on "action" entries only: the authored flow opened by that event. For the example, call event 1790724526599 and observe tray flow conf. This is reference metadata, not an option to pass to callEvent() or permission to bypass eligibility. |
integrationInfo.presets |
Family-qualified keys mapped to { label?, description, bindings }. Each binding may contain relationalId, slotId, label, description. An omitted slotId describes a slotless selection, not all slots. |
integrationInfo.trayBranches |
Flow ID -> branch ID -> { label?, description }. These identify whole-branch exclusions, not flow routing rules or individual-node exclusions. |
capabilities.languages |
nativeUI: translated built-in controls. experience: published project dictionaries for experience-specific text. Native UI support alone does not translate the experience’s authored text. |
capabilities.pdf |
Whether the published swapping data has configurationDocument. Check this before offering PDF generation; generation still needs its normal runtime readiness. |
customerNotes |
Public notes only, as strings keyed by event, family-qualified preset, or flow/branch. Private and orphaned notes are omitted. The response contains no note-visibility flags. |
warnings |
Array of { path, message } for stale/invalid catalog references. A published experience can have catalog warnings; do not assume those entries are usable. Report them to the customer for correction. |
Only Studio-selected integration entries are listed; internal initialization events and other undocumented actions need not appear. A listed action is not necessarily available at the current scene state. Let managed controls and safe callEvent() enforce runtime eligibility.
Older catalogs may omit configurationFlowId. If the opening event or its mapping is missing, ask Studio to add it and republish the catalog; do not infer it from a label or the first trayBranches key. Missing flows or mismatched event-to-flow mappings produce warnings without blocking project publication. The catalog does not enumerate individual nodes or flow routing rules.
For example, color:vc_01 above becomes viewer.callEvent("vc_01", { target: "color", relationalId: "cs_01" }). For the shelf in SaA, use viewer.callEvent("vo_7", { target: "object", relationalId: "os_05", slotId: "SaA" }). Do not pass the family-qualified catalog key as the raw event ID.
Understand the Embed Snippet
A generated snippet may contain these parts:
| Part | Purpose | Integration rule |
|---|---|---|
| Container dimensions and responsive CSS | Reserve space before JavaScript and images finish loading. | Keep a measurable width and height. Do not hide the whole surface while waiting for 3D. |
<presenter-3d project-id="..."> |
Identifies the published experience. | Preserve the project ID. Use a normal HTML id for your DOM queries. |
data-p3d-instance-id |
Identifies one appearance of a project on the page. | Optional and generated when omitted; choose stable, distinct values when targeting placements. It does not come from the project reference. |
Static <img data-p3d-static-image> |
Provides the first visible image before the runtime loads. | Keep its URL, role, and framing markers consistent with later media registration. |
data-p3d-image-frame, sizing wrappers, and generated CSS |
Frame an exported poster correctly from the first paint. | Keep the generated structure together. Replacing it with a generic object-fit rule can introduce a jump. |
data-p3d-poster-framing |
Carries the project's published poster-framing snapshot. | Preserve the generated JSON. Do not invent or infer its values from an image. |
| Loader script or network-aware bootstrap | Loads the runtime appropriate for the published project. | Keep the supplied loader URL and retry behavior. Do not copy a versioned runtime bundle into your page. |
customElements.whenDefined("presenter-3d") block |
Waits for the browser API to become available. | Put API setup here, after its target DOM elements exist. This is not 3D readiness. |
Regenerate the snippet from the dashboard after published poster-framing changes. Existing merchant HTML is not rewritten automatically.
Choose a Presentation
| Presentation | Use it when |
|---|---|
| Direct viewer | The experience should proceed directly to 3D, with its native loading presentation. |
| Hybrid viewer | A single image or gallery should remain usable while 3D loads, then transition according to your selected policy. |
| Permanent flat surface | A project-backed card or gallery should remain 2D. Add flat; it does not initialize 3D or require a WebGL probe. |
The custom element is the recommended approach for the APIs in this guide. A standalone link is useful when the experience should open on its own page. An iframe is a separate integration: parent-page code cannot treat its viewer as a local custom element, and the message bridge does not expose every API here. Do not assume API parity or invent postMessage commands.
A Minimal Hybrid Integration
Replace YOUR_PROJECT_ID, the example variation IDs, image paths, and labels with values supplied for your project. The images below are ordinary product photos, not padded exported posters. For an exported poster, retain the dashboard-generated image wrappers, CSS, and framing metadata instead.
<style>
#experience-mount { width: 100%; height: 560px; }
#experience-viewer {
display: block;
position: relative;
width: 100%;
height: 100%;
}
#experience-viewer > img[data-p3d-static-image] {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: contain;
}
#experience-controls .p3d-is-selected { outline: 2px solid currentColor; }
#experience-controls [aria-disabled="true"] { opacity: 0.5; }
@media (max-width: 600px) {
#experience-mount { height: 420px; }
}
</style>
<div id="experience-mount">
<presenter-3d
id="experience-viewer"
project-id="YOUR_PROJECT_ID"
data-p3d-instance-id="main-experience"
lang="en"
wait
>
<img
src="/images/variant-one.webp"
alt="Product in the first finish"
width="1000"
height="1000"
data-p3d-static-image
data-p3d-image-role="variant"
data-p3d-variant-id="vc_01"
data-p3d-image-framing="standard"
>
</presenter-3d>
</div>
<div id="experience-controls">
<button type="button" data-p3d-variation-id="vc_01">First finish</button>
<button type="button" data-p3d-variation-id="vc_02">Second finish</button>
</div>
<p id="experience-status" role="status">Loading interactive view...</p>
<!-- Retain the network-aware bootstrap instead when one is supplied. -->
<script
src="https://runner.presenter3d.com/loader.js"
data-project-id="YOUR_PROJECT_ID"
async
></script>
<script type="module">
const viewer = document.getElementById("experience-viewer");
const status = document.getElementById("experience-status");
const projectId = viewer.getAttribute("project-id");
const bindings = [];
const slowLoadNotice = setTimeout(() => {
status.textContent = "The interactive view is taking longer to load.";
}, 15000);
try {
await customElements.whenDefined("presenter-3d");
clearTimeout(slowLoadNotice);
if (!viewer.isConnected) throw new Error("The experience was removed.");
const api = window.presenter3d.commerce;
api.registerVariations(projectId, {
instanceId: "main-experience",
behavior: "slider",
viewerBehaviorOnSlider: "focus",
variations: [
{
variantId: "vc_01",
label: "First finish",
variantImageUrl: "/images/variant-one.webp",
variantImageFraming: "standard",
},
{
variantId: "vc_02",
label: "Second finish",
variantImageUrl: "/images/variant-two.webp",
variantImageFraming: "standard",
},
],
});
bindings.push(api.externalUIManager({
projectId,
instanceId: "main-experience",
containerElementId: "experience-controls",
}));
bindings.push(api.viewerProgressManager({
projectId,
onLoadingStart() { status.textContent = "Loading interactive view..."; },
onProgress({ progress }) { status.textContent = `Loading: ${progress}%`; },
onViewerIsReady() { status.textContent = ""; },
onViewerUnavailable({ message }) { status.textContent = message; },
}));
await viewer.start();
} catch (error) {
clearTimeout(slowLoadNotice);
status.textContent = "The interactive view could not be started. Images remain available.";
console.error("Presenter3D setup failed", error);
}
// For an SPA, run equivalent cleanup from the component's unmount hook.
window.addEventListener("pagehide", event => {
if (event.persisted) return; // Keep bindings when the browser caches this page.
clearTimeout(slowLoadNotice);
for (const binding of bindings) binding.dispose();
});
</script>
The example controls are not marked disabled: their binding manages that state. A loader failure before custom-element registration cannot emit viewer events. The delayed notice above reports a slow loader, not a definitive WebGL failure; keep the generated bootstrap's network recovery behavior for production.
In an SPA, also cancel pending setup and the notice timer when unmounting. Reinitialize bindings when remounting. Handle browser back/forward-cache restoration if you dispose bindings on pagehide.
Initialization and Readiness
| Stage | Safe work |
|---|---|
| Before runtime registration | Show static markup; reserve layout; set initial attributes and loading/privacy policy. |
| Custom element defined, DOM targets mounted | Register media, bind external controls and progress, register an external tray, and call start() when appropriate. Native commerce can also be registered at this stage. |
| Viewer initializing | Keep loading and native access/startup dialogs accessible. Do not assume scene actions, sharing, or PDF generation are available. |
| Viewer ready | Invoke supported 3D actions. Operations that require an idle viewer can still reject while another action is running. |
| Viewer unavailable | Keep flat media usable, stop your loading presentation, and explain the limitation. Do not mark 3D ready. |
Subscribe before starting. The progress options define callbacks and payloads. 3D, checkout and document features load on demand; merchant code does not manage their bundles.
Loading Gates
wait participates in the existing 3D startup policy and supports an explicit viewer.start(). It does not defer downloading the loader itself and is not a permanent pause or privacy boundary; a page where core is already active can follow the normal observer startup path.
For page-level deferral, configure the policy before the loader:
<script>
window.__PRESENTER_PASSIVE__ = {
loader: { network: "defer", onPageDone: true, delayMs: 1200, runOnObserve: true },
core: { onPageDone: true, delayMs: 1200 },
commerce: { network: "early" },
};
</script>
These are separate gates: loader networking, heavy 3D startup, and native business-data networking. runOnObserve can release the deferred loader when a tag becomes visible. Use commerce.network: "defer" only when native business requests should wait too. A deferred loader cannot expose APIs until it has actually loaded. Do not add this policy to the quick start unless you intend that behavior.
API Availability and Scope
| Operation | Required state | Commerce registration |
|---|---|---|
Read integration reference with getIntegrationInfo() |
Browser API available; integration-info password when enabled; no mounted/ready viewer required | Not required |
| Media, external-control, progress, live-text and tray registration | Custom element defined; target DOM mounted | Not required |
| Read tray state or inspect external UI state | Matching API exists | Not required |
| Call scene events or explicit presets | Loaded viewer; current action must be eligible | Not required |
| Reflect an existing viewer business summary | Summary available, or wait for a later summary | Not required |
| Fetch native business overview, resolve native markets, open native checkout | Authorized native business context; not 3D readiness | Required |
| Save/restore a shared configuration; preserve configuration on language/market change | Connected, ready, idle, server-backed 3D viewer | Sharing itself does not require registration; any registered authorization is reused |
| Generate a PDF | Connected, ready, idle 3D viewer with an authored document | Sharing authorization applies when requested |
Only one non-flat viewer per project is supported. Other same-project surfaces must be flat. Instance IDs target presentation, not independent configurations. Most bindings are synchronous and return handles; dispose each handle during teardown. Promise-returning operations require error handling even after a readiness check.
Images, Posters, and Galleries
An image's role and framing are different:
data-p3d-image-role="poster"identifies the initial poster; a registered image can reuse it when its URL matches.data-p3d-image-role="variant"withdata-p3d-variant-ididentifies a particular variation; reuse matches its variation ID.standardframing is for ordinary photos and uses contain-style sizing.posterframing is for exported captures with the project's padding/framing. It requires the generated framing snapshot for an accurate match.
Registered media uses variantImageFraming or productImageFraming, corresponding to the URL provided. Unmarked images default to standard; URL matching alone does not establish poster framing. All poster-framed images on a surface use that surface's published snapshot. Matching dimensions cannot make a photograph or a different camera/configuration state visually identical to the 3D view.
Media Registration and Initial Finish
api.registerVariations(projectId, registrationOrArray) is synchronous; array shorthand uses project-wide strict mode. Object form accepts every field below:
| Field | Default / values |
|---|---|
instanceId |
Optional; omitted registers project-wide media. Instance lookup falls back to project registration. |
behavior |
"strict" by default, or "slider". Strict entries require a variant ID; slider also supports gallery-only entries. |
variations |
Required array of media entries. |
viewerBehaviorOnSlider |
"focus" by default; "focus-and-lock" or "passive" also supported in slider mode. |
viewerHandoff |
"finalized" by default; "preview" additionally requires tag show-preview. |
slideInterval |
Positive seconds for autoplay; omitted/0 disables. Interaction stops it. |
removeFlatVariantsOnPassive |
True by default; applies only to "passive" slider handoff. False retains other variant media; the image in the viewer's own slide is still replaced by 3D. |
background |
Optional CSS background for the flat presentation. |
Each entry accepts variantId?, label?, variantImageUrl?, variantVideoUrl?, productImageUrl?, variantImageFraming?, productImageFraming?. Framing is "standard" (default) or "poster"; poster requires the generated snapshot. Use a real project variation ID when an image should apply a configuration; omit it for gallery-only media.
Choose the Gallery Handoff Behavior
With behavior: "slider", viewerBehaviorOnSlider controls what the visitor sees when the surface hands over to 3D. viewerHandoff separately controls when that handoff is eligible; neither setting replaces the startup/readiness requirements above.
| Mode | Handoff | Interaction and retained media |
|---|---|---|
"focus" (default) | Navigates to the selected variation's 3D slide automatically. | 3D is interactive when ready. Other variant-bound slides are removed; gallery-only images/videos remain navigable. |
"focus-and-lock" | Navigates to 3D automatically, then removes every other slide. | Only the 3D viewer remains. Gallery navigation is locked, not interaction with the 3D viewer. |
"passive" | Does not navigate away from the current slide. If it is the selected variation's slide, that image is replaced by 3D in place; otherwise the visitor can navigate to the 3D slide. | Gallery navigation remains available when other slides remain. Gallery-only media is retained; other variant-bound media follows removeFlatVariantsOnPassive. |
For "passive", the default removeFlatVariantsOnPassive: true removes other variant-bound slides after handoff, except a currently visible one, which stays until the visitor leaves it. Set it to false to keep those other variant images/videos. In both cases, the selected variation's viewer slide replaces its flat image rather than duplicating it. Autoplay stops at handoff.
The first eligible entry with media determines the initial flat image. If it has a variant ID and no shared selection exists yet, it seeds the project selection. Registration itself is not a completed 3D selection command. Keep the static image, first entry and authored initial finish consistent; later saved/restored state or user selection may differ. Ready business/configuration state can synchronize the selected variation. Do not force the first finish again in every readiness callback and overwrite a restored setup.
api.registerVariations(projectId, {
instanceId: "main-experience", behavior: "slider",
viewerBehaviorOnSlider: "passive", removeFlatVariantsOnPassive: false,
viewerHandoff: "finalized", slideInterval: 0, background: "#eeeeee",
variations: [
{ variantId: "vc_02", label: "Initial finish", variantImageUrl: "/images/finish-two.webp" },
{ variantId: "vc_01", label: "Other finish", variantImageUrl: "/images/finish-one.webp" },
{ label: "Detail photograph", productImageUrl: "/images/detail.webp" },
],
});
| Method | Return / use |
|---|---|
getVariations(projectId, instanceId?) |
Registration copy or undefined. |
getSelectedVariation(projectId) |
Selection copy or undefined; includes selectedVariantId, variation, project and presentation metadata. |
selectVariation(projectId, variantId) |
Synchronously updates registered shared media selection; not the requested viewer action. |
callVariation(projectId, variantId, options?) |
Promise of selection; drives the adapter when available, otherwise the existing flat/business path. Not proof of completed 3D work before readiness. |
unregisterVariations(projectId, instanceId?) |
Remove the specified media registration. Do not remove another component's registration. |
Focus-and-lock handoff can lock subsequent gallery selection. Invalid project/unregistered variant IDs throw. There is no special initialVariantId registration option.
api.callVariation() forwards supported options to the loaded viewer event adapter, for example { startFrom: "current", runWhenOffscreen: true }. It returns a media-selection object, not an event completion receipt. Flat-only selection has no scene animation to run.
Direct api.callVariation() and viewer.commerce.callVariation() never accept target, slotId, relationalId, flowId or stepId: any supplied value other than undefined is rejected before checking the viewer, even when 3D is ready. For explicit context, use viewer.callEvent() on a ready viewer. Managed variation buttons route their own context; do not replace that routing with a direct callVariation() call.
// Registered media selection, with supported viewer event options:
await api.callVariation(projectId, "vc_01", { startFrom: "current", runWhenOffscreen: true });
// Explicit preset/group context requires ready 3D:
await viewer.callEvent("vc_01", { target: "color", relationalId: "cs_01", runWhenOffscreen: true });
Connect External Controls
Create the controls, then bind their container with externalUIManager() as in the quick start. Use project-provided IDs:
<div id="additional-controls">
<button type="button" data-p3d-event-id="YOUR_EVENT_ID">Show detail</button>
<button type="button"
data-p3d-event-ids="YOUR_OPEN_EVENT_ID YOUR_CLOSE_EVENT_ID"
data-p3d-populate-text-content>
<span data-p3d-role="label">Open</span>
</button>
<button type="button" data-p3d-native-action="homeCamera">Home view</button>
<button type="button" data-p3d-native-action="isFullscreen">Fullscreen</button>
<button type="button" data-p3d-native-action="isSoundEnabled">Sound</button>
<input aria-label="Position" type="range" min="0" max="100" value="0"
data-p3d-event-id="YOUR_LIVE_CONTROL_EVENT_ID" data-p3d-live-control-slider>
</div>
const controls = api.externalUIManager({
projectId,
containerElementId: "additional-controls",
});
// After replacing children in the same container:
controls.refresh();
// On teardown:
controls.dispose();
External Controls and Live Text
api.externalUIManager({ projectId, containerElementId, instanceId? }) returns { refresh(), dispose() }. Project and container are required; instance scopes media/label lookup, while core lock/availability is project-scoped. Call refresh() after replacing child controls in the same container. Use a new binding for a different container.
| Attribute | Meaning |
|---|---|
data-p3d-variation-id |
Registered flat/3D variation action. |
data-p3d-event-id |
One event/direct option command. |
data-p3d-event-ids |
Space/comma-separated or JSON-array IDs; calls the currently available member. |
data-p3d-native-action |
Supported native action name. |
data-p3d-live-control-slider |
Range input mapped from its min/max into a normalized phase. Release/change/blur stops interaction. |
data-p3d-live-control-invert |
Boolean; invert the range mapping. |
data-p3d-interactive-state, data-p3d-direction |
start/stop and 1/-1 for authored interactive actions. |
data-p3d-force-action, data-p3d-bypass-passive-video-cancel |
Advanced booleans forwarded to event handling. Event/preset forcing defaults to false. Leave both unset for ordinary controls. |
data-p3d-target, data-p3d-slot-id, data-p3d-relational-id |
Explicit preset family (object, color, material), object slot and relational group. Use the catalog binding when context is needed. |
data-p3d-flow-id, data-p3d-step-id |
Optional paired flow/node context, with a matching relational group. Adds that node’s prerequisites; ordinary group-bound controls do not need it. |
data-p3d-action-state, data-p3d-action-data |
Native action boolean and JSON payload. |
data-p3d-sync-availability |
Boolean; event and variation controls default true. Omit it for safe controls. Explicit false stops availability synchronization, not the action’s eligibility checks. |
data-p3d-populate-text-content |
Populate registered/authored labels. |
data-p3d-label, data-p3d-labels |
Explicit label or JSON ID-to-label map. |
Child data-p3d-role="label" |
Text destination that preserves other button contents. |
The manager owns disabled/selected/available/running/active state, corresponding data-p3d-* attributes, .p3d-is-* classes, ARIA and form-control properties. Do not initially disable a managed button; that is treated as a merchant-owned disable. Do not attach both a managed event attribute and a custom handler that calls the same action.
An ordinary event button needs only data-p3d-event-id. The manager keeps it unavailable until the viewer reports that event eligible. Both after-event and on-start visibility rules, including variable-driven changes, are respected. Native-UI exclusions only hide the native representation; they do not disable an eligible external control. Do not recreate the experience’s scenario rules in page code.
Ready-3D preset controls use preset/group/slot eligibility, not whether a native options window is open. Registered variation media stays usable before 3D is ready and after startup failure; a flat image selection is not proof of 3D eligibility. Group-only controls remain independent of tray branch presentation. Add flow/node context only when the control should also follow that node’s prerequisites.
<!-- Bar finish: no force/sync flags or flow/node IDs are needed. -->
<button type="button" data-p3d-variation-id="vc_01"
data-p3d-target="color" data-p3d-relational-id="cs_01">Dark Walnut</button>
<!-- Bar shelf: specify the slot instead of guessing from the preset ID. -->
<button type="button" data-p3d-event-id="vo_7" data-p3d-target="object"
data-p3d-relational-id="os_05" data-p3d-slot-id="SaA">Shelf 4</button>
Place these inside the container bound by externalUIManager(); register media for vc_01 when using the variation attribute. Use your own project’s IDs, not these bar-specific examples.
api.subscribeLiveTagContent({ projectId, eventId, instanceId?, exposeTagContent?, preserveLastContent?, onContent }) returns { dispose() }. Both boolean options default false. Exposing text supplies localized, markup-stripped tagContent; preserving content avoids an inactive empty update at tag end. Payload: { projectId, instanceId?, eventId, keyframeId?, active, values, tagContent? }. Values are formula-adjusted project variables, not a universal measurement schema. This is not the tray’s immediate-state subscription contract.
Mirror Live Labels
To display an authored event's current localized tag text outside the viewer:
const label = document.getElementById("live-value-label");
const liveText = api.subscribeLiveTagContent({
projectId,
eventId: "YOUR_LIVE_CONTROL_EVENT_ID",
exposeTagContent: true,
preserveLastContent: true,
onContent({ tagContent }) { label.textContent = tagContent || ""; },
});
Create the label element before subscribing and call liveText.dispose() during teardown. Content is plain text from the currently rendered tag keyframe, not arbitrary project HTML. The callback can also include active, keyframeId, and formula-adjusted values. Use only value names supplied by the project; do not assume a universal measurement field or unit.
Inspecting Readiness and Calling Actions
viewer.getExternalUIState({ includeCamera?, runWhenOffscreen? }) is synchronous and observational. Both options default to false. It does not wake the viewer or grant permission to run an action.
| Field | Meaning |
|---|---|
runtime.ready |
Connected and prepared runtime eligible for execution under the inspection options. Independent of busy and renderPaused. |
runtime.loading |
Current preparation/resource-loading activity; check this separately. |
runtime.busy |
Current event/animation restrictions. |
runtime.renderPaused |
Rendering is paused; not by itself proof that a viewer cannot execute an external action. |
runtime.fullscreen |
Browser or simulated fullscreen state. |
runtime.passiveVideoActive |
Current passive/video-mode activity. |
runtime.unavailable |
Optional startup failure { projectId, instanceId?, reason, message }. |
pendingDialog |
Optional current native startup question, with dialogId, projectId, kind, question, instruction, and options: [{ id, label, effect }]. |
configuration |
Current serialized setup when the swapper exists; not a share ID. |
events |
Event-ID map of { scenarioVisible, disabled }. scenarioVisible combines both authored visibility layers and the existing parent mask; disabled events report false. It is not pixel visibility. |
eventIds, swapOptionIds |
Eligible scene events and currently exposed business-option identifiers, respectively. Runtime readiness/busy/flow checks still apply. Not a complete preset eligibility catalog. |
eventLabels, swapOptionLabels, variationGroupLabels, variationAliases, variationGroupIdsByVariationId |
Optional metadata; use IDs from the project’s integration reference rather than constructing them from labels. |
activeVideoModeEventId, stoppableEventIds |
Current video event and available stop metadata when present. |
camera |
Included only with includeCamera: true and an available camera; inspection does not move it. |
For an offscreen custom button, inspect with runWhenOffscreen: true to recognize a loaded, resumable standby scene. It does not make initial loading, disposal or an explicit pause ready. Recheck at click time; readiness can change after a callback.
const state = viewer.getExternalUIState({ runWhenOffscreen: true });
if (!state.runtime.ready || state.runtime.loading || state.runtime.busy || state.pendingDialog) {
throw new Error("The experience is not ready for this action.");
}
if (!state.eventIds.includes(setupEventId)) {
throw new Error("This action is not currently available.");
}
await viewer.callEvent(setupEventId, { runWhenOffscreen: true });
Check typeof viewer.callEvent === "function" during early setup. Use progress callbacks for load cycles; there is no general external-state subscription. A custom button needing continuous busy/dialog feedback may poll while mounted and clear that timer on teardown. Prefer externalUIManager() for ordinary buttons.
Event Options
viewer.callEvent(eventId, options?) returns a promise. Ordinary calls resolve without a universal completion receipt. Normal event, preset, live-control and interactive calls are distinct modes; do not combine incompatible modes.
Settlement Without reportResult
For the installed viewer.callEvent() method, successful calls and skipped calls both resolve to undefined. A resolved promise alone does not confirm that an action ran or a configuration changed.
| Call / condition | Promise outcome |
|---|---|
| Scene event hidden by either scenario layer, disabled event, ineligible configuration-opening flow, or a busy/paused runtime gate | Resolves to undefined without dispatching the requested scene action. These eligibility skips do not reject. |
| Configuration-preset event (predefined or custom) fails during application | Can resolve to undefined after failure: handleInteractedPointer() catches application errors. Use reportResult: true to detect these caught failures. |
| Resolved preset fails selection compatibility, regional availability, or optional flow/node eligibility; swapping is already busy | Resolves to undefined without applying the selection. Invalid or incomplete optional flow/node context also follows this eligibility-skip path. |
| Preset cannot be resolved in explicitly requested context; unknown slot/group, ambiguous required context, or unsupported direct selection | Rejects with a plain Error when selection validation is reached. A globally disabled preset is excluded from resolution, so an explicit selection can reject as having no selectable preset rather than resolve as an eligibility skip. Earlier runtime gates may skip the call before validation. |
| Explicit preset context while the tag is still loading | Rejects with a plain Error: direct explicit selection requires ready 3D. |
These rejections have no documented stable error.code; message text is diagnostic and must not be parsed as a machine contract. Catch errors, but also use managed control eligibility and observe the resulting state. A non-explicit call during loading may update an available flat/business selection without running a scene event; passive-video handling may defer an action until cancellation. Neither is a completed-3D-action guarantee. Direct api.callVariation() is different: it rejects when its preset-availability check returns false and always rejects the explicit-context options described above.
For supported regular events, including configuration-preset events for predefined and custom configurations, reportResult: true provides structured status/reason values instead. Supply a nonempty requestId of at most 128 characters and inspect the returned receipt; caught application errors produce status: "failed" with completionConfirmed: false. Keep try/catch for API validation and other propagated errors, but do not rely on rejection alone to detect configuration-application failure. There is no rollback guarantee after scene application starts.
Direct object/material/color preset selections are a separate path and do not support receipts. Configuration-opening events are also unsupported receipt operations; confirm configuration opening through the tray subscription, as in the recipe.
| Option | Values / behavior |
|---|---|
runWhenOffscreen |
Boolean; loaded resumable tags automatically hold external calls awake unless false. Explicit true is supported. Does not start an unloaded viewer or bypass eligibility/explicit pauses. |
startFrom |
"beginning" or "current"; omitted uses authored event policy then beginning. Current-position playback requires eligible authored phases. |
interactive |
{ state: "start" | "stop", direction?: 1 | -1 } for authored interactive controls. |
liveControlPhase |
Normalized number 0..1 for an authored live-control event. |
holdLiveControlPhase |
With a phase, true keeps a single-pointer animation paused at the applied phase. |
forceAction |
Defaults false for events and presets. Scene events must pass both authored visibility layers; presets must pass selection eligibility. True is an advanced override, described below. Leave unset for ordinary controls. |
bypassPassiveVideoCancel |
Advanced boolean; leave unset for user controls that should interrupt video mode. |
target, slotId, relationalId |
Explicit preset family, object slot and selection-group context on ready 3D. See below. |
flowId, stepId |
Optional preset context; supply both plus a matching relationalId. stepId must identify a node in that flow, not a branch. Adds its prerequisites without requiring the tray to be open. |
reportResult, requestId |
Opt-in regular-event receipt, including predefined/custom configuration-preset events; requires a nonempty correlation ID up to 128 characters. Not for direct object/material/color preset selections, configuration-opening events or continuous controls. |
executionContext |
Runtime restore/playback context; merchant user actions should leave it unset. |
await viewer.callEvent("YOUR_CAMERA_EVENT", { startFrom: "current" });
await viewer.callEvent("YOUR_LIVE_CONTROL", { liveControlPhase: 0.5, holdLiveControlPhase: true });
await viewer.callEvent("vo_1", { target: "object", slotId: "YOUR_SLOT", relationalId: "YOUR_GROUP" });
Explicit selection supports object, material and color families; explicit texture/shared selection is not supported by this API. Use the exact relationalId option, not variationGroupId. Use the integration reference's group/slot context for ambiguous presets. Direct callVariation() rejects all five explicit-context fields (target, slotId, relationalId, flowId, stepId) regardless of viewer readiness; use viewer.callEvent() instead. Authoring determines repeat-toggle and compatibility behavior.
Scene events use showAfterEvents/hideAfterEvents and showOnEventStart/hideOnEventStart, including variable-driven changes. Offscreen rendering, occlusion, closest-pointer limits and native-UI exclusions do not themselves block an external action. Stopping the active interactive event remains possible after it becomes hidden; this does not permit restarting it.
Advanced override: await viewer.callEvent(eventId, { forceAction: true }) deliberately bypasses scenario visibility and can violate the authored interaction sequence. A managed override button must explicitly set both data-p3d-force-action="true" and data-p3d-sync-availability="false". Disabling synchronization alone does not bypass the call guard. This is not an access-control override.
Preset selection uses the existing preset/group/slot validators by default. It does not require a visible scene pointer, native option icon, or open tray. Optional flowId/stepId adds a matching node’s prerequisites; it is not necessary just because a relational group also appears in a tray. Hiding the bar’s finishes branch therefore does not disable external cs_01 finish buttons. Do not treat swapOptionIds as a complete eligibility catalog.
For presets, explicit forceAction: true bypasses the external selection-eligibility and optional node-prerequisite guard. It does not make missing/disabled presets or invalid group/slot/flow context valid, nor bypass runtime gates. Keep it out of normal integrations.
For ordinary events, an opt-in receipt reports status (completed, unavailable, canceled, failed or unconfirmed), optional reason, completionConfirmed, and request/event/project/instance identifiers. Do not interpret an unconfirmed result or timeout as success or cancellation. The tray recipe confirms the resulting tray state instead of assuming that any resolved call opened it.
const receipt = await viewer.callEvent("YOUR_EVENT_ID", {
reportResult: true, requestId: "merchant-action-1", startFrom: "current",
});
if (receipt.status !== "completed" || !receipt.completionConfirmed) {
statusElement.textContent = receipt.reason || "The action could not be confirmed.";
}
Native Actions
viewer.callNativeAction(actionName, { actionState?, actionData?, bypassPassiveVideoCancel? }) returns a promise. actionState is an explicit boolean for stateful controls; actionData is action-specific, not arbitrary viewer configuration. Examples on a ready viewer:
await viewer.callNativeAction("isSoundEnabled", { actionState: false });
await viewer.callNativeAction("arePointersVisible", { actionState: true });
await viewer.callNativeAction("homeCamera");
| Action | Options / behavior |
|---|---|
homeCamera, resetViewer, stopCurrentEvent |
No action data; return to the authored camera, restart the viewer, or interrupt the current event. |
isFullscreen |
actionState: true/false enters/exits; omitted toggles. Call from a user gesture; an available fullscreen control/browser capability is required. |
isSoundEnabled |
Explicit actionState: true/false; omitted is false for a direct API call. |
arePointersVisible, isEnvironmentVisible |
Explicit visibility; omitted is true for a direct API call. |
infoTabExpanded, offerTrayExpanded |
Expand/collapse an existing native panel; omitted is true. |
infoBoxVisible, poweredByVisible |
Open/close the native information/about dialog; omitted is true. Does not remove branding from the viewer. |
sidebarScroll |
actionData: { position, direction }; position is "left", "right", "top" or "bottom"; direction is "scrollForward" or "scrollBackward". Requires that sidebar. |
tagClose |
actionData: { eventId } closes that event's tag; legacy e is also accepted. |
Use only controls authored for this project. A resolved call is not proof that a control existed or changed. Performing an action is not a hide-native-button API; use tray presentation exclusions below.
viewer.respondToDialog({ dialogId, optionId }) answers only the currently active matching dialog. Read its actual options; do not hardcode yes/no IDs. It does not hide the native prompt or enable an external-only mode. Keep native dialogs accessible.
It returns synchronously: { status: "answered", ... } or { status: "unavailable", reason: "dialog_no_longer_pending" | "invalid_dialog_option", ... }, with dialog/option/project/instance identifiers. Inspect again after answering; another dialog or loading may follow.
// Run only when the visitor chooses an option rendered from this snapshot.
const dialog = viewer.getExternalUIState().pendingDialog;
if (dialog && dialog.options.some(option => option.id === chosenOptionId)) {
const answer = viewer.respondToDialog({ dialogId: dialog.dialogId, optionId: chosenOptionId });
if (answer.status !== "answered") statusElement.textContent = "This question has changed. Please try again.";
}
Camera Navigation and PNG Capture
Both APIs require a connected, ready, idle scene. runWhenOffscreen: true permits an already-loaded, resumable scene; it is not a loading bypass.
viewer.navigateCamera(options) option |
Type / default |
|---|---|
view |
Optional { cameraCoords?, lensZoom? } baseline; omitted uses the current camera. |
orbitDegrees |
Number -180..180; default 0. Positive follows the viewer's right-arrow orbit. |
elevationDegrees |
Number -90..90; default 0. Positive raises the viewpoint. |
distanceScale |
Number 0.25..4; default 1. 0.8 moves 20% closer. |
lensZoom |
Number 0.1..10; overrides view.lensZoom, otherwise keeps the current value. |
runWhenOffscreen |
Boolean; default false. |
Returns a promise of { status: "completed" | "unavailable" | "canceled", reason?, projectId?, instanceId?, limited?, camera? }. Invalid arguments reject. Authored navigation limits still apply; limited reports clamping. Fullscreen, passive video, walkthrough, playback or active user/camera navigation can make navigation unavailable.
viewer.captureImage(options?) option |
Type / default |
|---|---|
view |
Temporary { cameraCoords?, lensZoom? }; omitted captures the current view. |
freeCamera |
Detached perspective camera, mutually exclusive with view: required position, target, up vectors { x, y, z }, verticalFovDegrees, near, far; optional rollDegrees. |
size |
{ width, height } in positive integer pixels; omitted uses viewer dimensions. |
preserveBackground |
Boolean; default false. |
hideSkybox |
Boolean; hide the skybox mesh only for this capture. |
runWhenOffscreen |
Boolean; default false. |
Returns Promise<Blob> containing PNG; it does not download automatically. Temporary capture state is restored before encoding. cameraCoords accepts cranePosition: { x, y, z }, craneZoom, panAndTruck and tiltAndPedestal; these authored coordinates use radians for angles, unlike the degree-based relative navigation options. Use known authored coordinates; if none were supplied for your integration, omit the override rather than guessing scene units.
const navigation = await viewer.navigateCamera({ orbitDegrees: 15, distanceScale: 0.9 });
if (navigation.status === "completed") {
const image = await viewer.captureImage({ size: { width: 1200, height: 1200 }, hideSkybox: true });
// Preview/download the Blob; revoke any object URL when it is no longer used.
}
Configuration Tray Registration
api.registerConfigurationTray({ projectId, ...options }) returns a tray binding. The equivalent viewer.commerce.registerConfigurationTray(options) supplies the project from the tag. Registration neither opens a flow nor starts 3D.
The external host must exist in the same document, have measurable width, be outside all viewer tags, and not contain a viewer. Only one binding per viewer is supported; another registration replaces the previous one.
All Host Options
| Option | Type/default | Effect |
|---|---|---|
projectId |
Required string on the global method | Exactly one non-flat viewer must match. Not part of tray.update() options. |
containerElementId |
Optional string | External destination. Omit for presentation exclusions without relocating the tray. |
maxHeight |
Positive number; 380 |
CSS-pixel height budget. Fixed mode treats it as a ceiling, not a target height. |
heightMode |
"content" (default) or "fixed" |
Content shrinks with fewer items; fixed retains a capacity-based baseline while expanded. |
visible |
Optional boolean; omitted does not hide | false hides a retained flow without closing it. true reveals it but does not open a closed flow. |
visibilityPolicy |
"externalOnly" (default) or "shared" |
Shared carries the host's visibility intent into fullscreen and synchronizes explicit native opens/closes. |
collapsedHeight |
Nonnegative number; 0 |
CSS-pixel space retained by a hidden external tray. |
expansionDurationMs |
Nonnegative number; 300 |
External height transition in milliseconds; 0 disables it. Reduced-motion preference disables motion. |
panelCorners |
Object of nonnegative pixel values | topLeft, topRight, bottomLeft, bottomRight; omitted corners inherit authored styling. |
showHeader |
Optional boolean | false removes the title/size/close row, not navigation. |
hiddenControls |
Array; none by default | Any of home, download, previous, next, close, minimize, maximize, restore. Applies to tray controls, not all viewer UI. |
optionsWindowBehavior |
"authored" (default) or "keepOpen" |
Keep the external chooser beside a linked options window. The window itself remains inside the viewer. |
settings |
Restricted presentation object | Allowed fields are listed below. Not a general viewer-settings API. |
nativeExclusions |
Array of explicit rules | { eventId, target: "sidebar" or "viewerPointer", restoreOnFullscreen: boolean }. Each presentation needs its own rule. |
excludedBranches |
Array of explicit rules | { flowId, branchId, restoreOnFullscreen: boolean }. Only nested Branch containers and descendants are omitted from navigation. |
visuals |
Flow ID, then entry ID map | Header/branch visual replacements: { imageUrl?, iconStyle? }. |
presetVisuals |
"target:presetId" map |
Option-thumbnail replacements: { imageUrl?, iconStyle? }. Example key: "object:vo_1". |
Exclusions do not remove selections, change business eligibility, or remove PDF contents. restoreOnFullscreen: true removes host suppression in fullscreen; it does not override authored disabling or scenario visibility. Pointer suppression does not hide product geometry or block external event calls.
Retained Visibility and State
Use visible to collapse the presentation, not to close the configuration flow. A hidden retained flow keeps its current route and selections. Replaying the setup event on every expansion can restart navigation; call it only when getState().open is false.
With visibilityPolicy: "shared", native/event open sets host visibility to true; native close sets it to false and closes the flow normally. Refreshes, resizes and options-window restoration do not reopen a tray deliberately hidden by the merchant. Hiding does not close an already-open options window. Fullscreen continues to use native styling/layout.
const trayOptions = {
containerElementId: "configuration-panel",
visible: false,
visibilityPolicy: "shared",
heightMode: "fixed",
maxHeight: 400,
settings: { heightCapacity: 2 },
};
const tray = api.registerConfigurationTray({ projectId, ...trayOptions });
const unsubscribe = tray.subscribe(state => {
setupButton.textContent = state.visible ? "Close settings" : "Configure";
setupButton.setAttribute("aria-expanded", String(state.visible));
trayHost.dataset.expanded = String(state.visible && state.destination === "external");
});
subscribe(listener) invokes the listener immediately, then queues changed snapshots. Synchronous updates can coalesce. It is a state subscription, not an action log or animation-completion event. Do not reference the not-yet-assigned unsubscribe variable inside its immediate callback.
| Snapshot field | Meaning |
|---|---|
projectId |
Binding project. |
flowId |
Active flow ID; absent when closed. |
open |
A flow exists, including when hidden. |
visible |
Logical visibility under the current host policy; not pixel visibility, scrolling, occlusion, or animation completion. A minimized native tray is visible. |
fullscreen |
Browser or simulated fullscreen, even when the flow is closed. |
destination |
"external", "native", or null when closed. A hidden retained flow still has a destination. |
size |
"default", "minimized", "maximized", or null when closed. External presentation reports "default", regardless of fixed/content mode. |
getState() returns a detached snapshot without starting core or changing state. Before core exists it reports closed/not visible. update(options) replaces host options; omitted options return to their defaults/authored values, including exclusions. Preserve the full options object, but read current visible before unrelated updates so a stale initial false does not collapse a tray opened in fullscreen:
tray.update({ ...trayOptions, maxHeight: 440, visible: tray.getState().visible });
Keep your options variable updated too if later updates should retain the new height. Policy updates are synchronous; layout can refresh on the next frame. Unsubscribe is idempotent. Disposal or replacement removes subscriptions without a final callback. getState(), subscribe() and update() throw after disposal; repeated dispose() is harmless. Disposal restores native placement and removes host exclusions.
Capacity, Height and Fullscreen
settings.heightCapacity: 2 is a number, not { capacity: 2 }. It resolves through authored/adaptive settings to a default of 3; finite values are rounded down and clamped to at least 1. Capacity estimates rendered body rows using icon size, padding, spacing and layout. Several tiles on one grid row count as one row.
In external fixed mode the first screen establishes a full-capacity baseline plus its measured header. Later content may grow above that baseline up to the capacity and maxHeight, then shrink back. Extra rows scroll. A 400px cap does not guarantee two rows when the selected design needs more room. Empty screens do not reserve imaginary tile rows. Content mode can exceed its height budget for the minimum usable header/row; fixed mode respects the ceiling.
Hiding retains the baseline; a new/closed flow, host replacement, width change or sizing-settings change can reset it. Fullscreen navigation retains it. External layout is always normal size; native minimized/maximized size is retained separately for fullscreen. Browser and simulated fullscreen move the tray to the native viewer; exiting restores its external destination. A disconnected host falls back to native placement on refresh; call update() after replacing the host element.
The runtime animates open/collapse and content-height changes with ease-in-out, honors reduced motion and continues from the current rendered height. Do not add a second JS height animation. It reserves a 2px depth/progress slot and, in horizontal non-minimal headers, a subtitle line even when empty. Empty slots are hidden from accessibility output. Header visuals keep their slot unless showHeaderIcon: false; compact headers use their native space-saving behavior.
Settings and Styling
External overrides merge over effective project settings. They do not restyle native floating options windows or replace fullscreen styling. Unsupported settings keys are rejected.
| Allowed field | Values / purpose |
|---|---|
headerType |
"auto", "regular", "minimal"; authored default auto adapts to available space. |
showHeaderIcon |
Boolean; false removes the header visual slot. |
navigationIconSize |
Legacy CSS-pixel fallback; effective options.iconSize takes precedence. |
heightCapacity |
Numeric row capacity as described above. |
downloadPdf |
Boolean; native PDF action still needs an authored document. False does not disable external generation. |
fontSize |
Pixel sizes: title, subtitle, content, small; defaults 14, 12, 13, 11. |
options |
Option layout settings listed below. |
style |
accentColor, backgroundColor, borderColor, textColor, mutedTextColor, fontFamily, borderRadius, opacity, shadow. |
panelStyle |
backgroundColor, borderColor, textColor, mutedTextColor, borderRadius, opacity, shadow; matching fields override effective style. |
scrollBarSetting |
Existing scrollbar presentation settings; not a height or navigation API. |
Option layout fields are layout: "rows" | "tiles", iconShape, tilesIconShape, rowsIconShape ("round" | "square"), squareRoundedRadius, iconSize, iconPadding, itemPadding, gap, maxColumns, and selectedBorderColor. Defaults are rows, round, square radius 4px, icon size 64px, icon padding 0, item padding 8px, gap 12px; columns fit available width unless capped. Padding/gap/radius are nonnegative; sizes/column caps must be usable positive values. Layout-specific shape overrides generic shape. Project entry options can also affect the rendered design.
scrollBarSetting accepts thumbColor, displayOnHover, transitionDuration (seconds), thumbWidth (pixels), sideOffset (pixels), side: { vertical: "left" | "right", horizontal: "top" | "bottom" }, thumbHeightScale (0.01..1), and track: { color?, width?, disabled? }. Omitted fields inherit global scrollbar settings, including nested side/track fields. Tray side offset defaults to 0 unless explicitly overridden. These settings do not reserve extra layout space or alter capacity.
Primary and secondary text are separate. Secondary-color precedence is effective panelStyle.mutedTextColor, then effective style.mutedTextColor, then renderer fallback #c3ad96. Because overrides merge, an authored panel muted color still wins over a host change to style.mutedTextColor. Set it in panelStyle when you need to override it. Whole-tray style.opacity multiplies panelStyle.opacity (each 0..1).
const lightOptions = {
...trayOptions,
visible: tray.getState().visible,
expansionDurationMs: 300,
panelCorners: { topLeft: 8, topRight: 8, bottomLeft: 8, bottomRight: 8 },
settings: {
...trayOptions.settings,
style: { fontFamily: "Arial, sans-serif", accentColor: "#527b70" },
panelStyle: {
backgroundColor: "#f7f7f7", textColor: "#242424",
mutedTextColor: "#646464", borderColor: "transparent", shadow: false,
},
options: { layout: "rows", iconSize: 56, itemPadding: 8, gap: 10 },
},
visuals: { conf: { interior: { imageUrl: "/images/interior.webp" } } },
presetVisuals: {
"object:vo_1": { imageUrl: "/images/classic-handle.webp" },
"color:vc_01": { iconStyle: { color: "#314434", borderColor: "#202020" } },
},
};
tray.update(lightOptions);
Retain lightOptions for later replacements. The flow/entry IDs conf and interior and preset keys are examples. Read actual flow/branch and preset IDs from the integration reference. For a node/header entry such as interior, obtain the exact ID separately from Studio or an explicit description/public customer note; getIntegrationInfo() has no structured node catalog. Omit visuals if no matching entry ID is supplied. visuals does not use event IDs as a substitute for flow-entry IDs. presetVisuals uses the preset ID, not SKU or displayed label, and the exact colon separator. iconStyle accepts an existing project-supported pattern, svgColor (symbol), color (background), and borderColor; do not invent pattern names. URLs are merchant-hosted images, not project resource hashes or persisted textures.
Host-Page Scroll Pausing
api.setPauseOnHostScroll(true); // No event/audio exemptions.
api.setPauseOnHostScroll(true, { noPauseOn: ["eventRuns", "activeAudio"] });
api.setPauseOnHostScroll(false); // Default: disabled.
This synchronous, page-wide setting needs no token or network request. Options replace the previous options; omitted/empty noPauseOn means no exceptions. Either selected exception keeps an affected viewer running. eventRuns includes running animations and initial/idle video sequences. activeAudio means currently playing viewer background/event audio with sound enabled, including audio outliving its event. Scheduled cues do not count; remaining delays are preserved while paused. A loop can keep the viewer exempt while it plays.
This does not control unrelated page media or internal viewer scrolling/zoom. Manual pause, checkout and viewport constraints still apply; active held external actions use their existing wake protection. Configure it once per page, not independently in competing components. Disable it on page-level teardown only if your integration owns that policy.
Loading Progress and Failure Notices
api.viewerProgressManager(options) returns { dispose() }. It requires projectId and at least one output target/callback.
| Option | Default / purpose |
|---|---|
elementId |
Optional text target receiving progress such as 74%. |
wrapperElementId |
Optional wrapper receiving a pointer-inert SVG progress border. |
progressBar |
True/default options or object; false disables border. A wrapper enables it unless false. |
completionHoldMs |
300ms at completed progress before clearing UI/readiness callback. |
onLoadingStart, onViewerStart, onViewerIsReady |
{ projectId, cycleId }; start is 100%, ready follows the hold. |
onProgress |
{ projectId, cycleId, progress, target }; smoothed completed percent 0..100; target preview/finalized. |
onStageChange |
{ projectId, cycleId, stage }; request/resources/scene/preview/textures/finalized. |
onViewerUnavailable |
{ projectId, instanceId?, cycleId, reason, message }; also delivered to late subscribers after recorded startup failure. |
Preview startup is a supported presentation choice: with show-preview, the experience can be shown while higher-resolution textures continue loading. Hybrid galleries can opt into this with viewerHandoff: "preview"; waiting for finalized presentation is not mandatory. The progress callbacks describe the current loading target: onViewerStart fires at 100%, and onViewerIsReady follows the completion hold. They are not a preview-versus-finalized pair. Use the progress target/stage to describe loading, and let managed controls follow runtime action eligibility. Sharing and PDF operations retain their own readiness requirements.
Progress-bar options: direction: "grow" | "shrink" (default shrink); startCorner (top-left, top-center, top-right, right-center, bottom-right, bottom-center, bottom-left, left-center; default top-center); startColor (default #df2d64), endColor (#3edb00), trackColor?, thickness (4px), zIndex?, className?.
const loading = api.viewerProgressManager({
projectId, elementId: "load-percent", wrapperElementId: "experience-mount",
completionHoldMs: 200,
progressBar: { direction: "grow", startCorner: "bottom-left", thickness: 3,
startColor: "#bd354d", endColor: "#238654", trackColor: "#cccccc" },
onViewerUnavailable: ({ message }) => { statusElement.textContent = message; },
});
Failure reasons are webgl2_unavailable and initialization_failed. Neither emits synthetic readiness. The raw bubbling viewerUnavailable event has project/instance/reason/message, not the manager's cycleId, and is not replayed.
The native fallback selector is [data-p3d-viewer-unavailable-notice]. Hide/style it only after providing your own visible, accessible notice for that same surface. Scope the rule to a merchant-owned wrapper around the whole experience and a marker set by your failure callback; never hide it globally at page startup. In hybrid mode the notice can be outside the tag itself; use the stable outer wrapper, not private runtime wrapper classes. Remove the marker on a new load or successful readiness, and on teardown. Preserve flat controls and remove misleading loading text. The recipe shows this pattern without observing private viewer DOM.
viewer.setCountdownVisible(false) hides the native numeric countdown; true requests it, and no argument restores the project default. Registered hybrid media suppresses it regardless. It is separate from loading-bar/updating-bar and reports remaining, not completed, percent.
Startup Dialogs and Failure Handling
Native startup dialogs are displayed above registered hybrid media. They may need an answer before the viewer can become ready:
| Dialog | Meaning |
|---|---|
session_restore |
Offers restoration of a saved configuration when the project policy permits it. |
session_restore rejection notice |
Explains why a saved/shared setup could not be applied; this notice is informational and does not block readiness. |
crash_recovery |
Asks whether a previous attempt really failed and whether optimization should be enabled. A stored marker is not proof of a crash. |
device_optimization |
Offers lower resource use when the project exceeds the effective device budget. |
Do not cover these choices with your own loading overlay, hide their DOM, or auto-answer them just to finish progress. The public API can inspect viewer.getExternalUIState().pendingDialog and answer through viewer.respondToDialog({ dialogId, optionId }), but cannot suppress the native dialog or put it into an external-only mode. There is no dedicated dialog-opened/closed subscription. Supplemental external UI must poll while mounted, use the actual question/options, and reconcile after answering. These inspection methods may not exist at the earliest initialization stage.
When 3D Cannot Start
Handle onViewerUnavailable alongside readiness, not as a progress value:
webgl2_unavailable: keep images/gallery usable and explain the browser/device limitation. Repeatedstart()calls do not retry a confirmed capability failure; reload after correcting the browser/device setting.initialization_failed: an initialization/module-loading failure, not proof of missing WebGL2. An explicit start may retry; preserve a usable fallback.
The tag also emits a bubbling viewerUnavailable event with { projectId, instanceId, reason, message } in detail. Raw DOM events are not replayed; a progress binding also reports a previously recorded startup failure to a late subscriber. Filter by project/instance when using shared listeners.
A failure does not emit synthetic readiness. Disable 3D-only actions but retain media controls. Native commerce can obtain authorized pricing independently of 3D; merchant-owned commerce continues using its own price source. Never leave an indefinite "Loading price" message or substitute zero for missing data.
Reflecting an Existing Business Summary
For authorized product loading, quantity, markets and checkout, see native commerce method options. This section covers token-free display binding.
api.reflectBusinessData(options) renders available summary data and listens for updates. It does not require api.register() merely to display a ready viewer's existing summary. It does not turn that summary into authorized market/stock data. Independent native overview/product requests and checkout require native registration. Without a summary, a reflection binding leaves placeholders in place.
| Option | Purpose |
|---|---|
projectId |
Explicit project recommended; omission tries single-project resolution. |
locale |
Formatting override; omitted follows runtime language then browser locale. |
basePriceElementId, effectivePriceElementId |
Amount destinations; EffectivePriceId is a legacy alias for the latter. |
basePriceConfig, effectivePriceConfig |
Amount formatting object below. |
discountElementId, deliveryElementId, stockElementId |
Optional plain-text destinations. |
discountConfig, deliveryConfig, stockConfig |
Optional text-injection formatting below. |
All text configs accept optionalTextToInject and optionalInjectionLocation: "before" | "after" (default after when text is supplied). Amount configs additionally accept currency: "prepend" | "append" | "none", crossedOut, showOnlyOnDiscount. Use explicit currency placement when desired; never use currency hiding as conversion. Discount display can be percent for one selected line or saved amount for multiple lines.
const prices = api.reflectBusinessData({
projectId,
basePriceElementId: "base-price",
basePriceConfig: { crossedOut: true, showOnlyOnDiscount: true },
effectivePriceElementId: "price",
effectivePriceConfig: { currency: "append", optionalTextToInject: "Configured price: ",
optionalInjectionLocation: "before" },
stockElementId: "stock", deliveryElementId: "delivery",
});
// Optional: seed from an already-ready viewer; later events continue updating it.
const summary = viewer.getBusinessDataSummary?.();
if (summary) prices.refresh(summary);
Return value: { refresh(summary?), dispose() }. refresh() rerenders existing state; it is not a server refetch. viewer.getBusinessDataSummary() can be absent early or return null. Do not format missing data as zero. This binding is a display utility, not a general adapter for an unrelated store's JSON or payment authority.
Shared Configurations and PDF Files
Authored Suggestions And My Configuration
An experience can offer predefined configurations, such as Wine storage and Custom shelving, plus one automatically retained My configuration. These are optional authored features. Use the action event IDs and descriptions supplied in the project's integration reference; if they are missing, ask Studio to include them. Do not infer IDs or configuration contents from button labels.
The merchant page invokes existing events. It does not upload configuration definitions, manually save the custom snapshot, or need Commerce Engine registration for this feature.
| Identifier / definition | Merchant integration |
|---|---|
| Action event ID | Pass to viewer.callEvent() or data-p3d-event-id. Each predefined configuration and the custom restore action has an authored event. |
configurationPresetId / stateId | The event's internal link to an entry in swaps.predefinedStates; do not pass the state ID instead of the event ID. |
isCustomConfigPlaceHolder: true | Authored event marker for restoring My configuration. This is not an HTML attribute or an option for callEvent(). |
configurationFlowId | A different feature: opening the configuration tray. It does not apply a predefined configuration. |
Managed Configuration Buttons
Keep the generated viewer/loader and its startup code. Add this markup outside the viewer, replacing all three event-ID placeholders with actions from your project's reference:
<div id="configuration-presets" role="group" aria-label="Product configurations">
<button type="button" data-p3d-event-id="YOUR_FIRST_CONFIGURATION_EVENT_ID">Wine storage</button>
<button type="button" data-p3d-event-id="YOUR_SECOND_CONFIGURATION_EVENT_ID">Custom shelving</button>
<button type="button" data-p3d-event-id="YOUR_CUSTOM_CONFIGURATION_EVENT_ID">My configuration</button>
</div>
<p id="configuration-status" role="status" aria-live="polite"></p>
Bind once after the markup exists. This example uses the quick-start viewer's experience-viewer ID:
await customElements.whenDefined("presenter-3d");
const presetViewer = document.getElementById("experience-viewer");
const presetControls = window.presenter3d.commerce.externalUIManager({
projectId: presetViewer.getAttribute("project-id"),
instanceId: presetViewer.dataset.p3dInstanceId,
containerElementId: "configuration-presets",
});
// Full-page teardown; retain the binding when entering the back/forward cache.
window.addEventListener("pagehide", event => {
if (!event.persisted) presetControls.dispose();
});
For an SPA, use the component's unmount hook to call presetControls.dispose() instead of registering the page-level listener. Call presetControls.refresh() after replacing buttons. Do not bind overlapping containers: if an existing externalUIManager already covers these buttons, refresh that binding instead of creating another one.
Leave data-p3d-force-action, data-p3d-sync-availability and literal disabled attributes unset. The manager handles eligibility, transient locks and running state. Do not attach another click handler to the same managed button or pass these event IDs to callVariation(). These actions require a ready 3D viewer; they do not apply a whole configuration to the flat gallery before 3D is ready.
My configuration is unavailable until a custom snapshot exists. It still respects disabled events and both scenario-visibility layers. Suppressing a native pointer through the native UI hosting options does not itself disable the external action. The manager rereads the public availability snapshot after notifications; do not recreate these rules in page code. Use the existing disabled/available CSS states to decide whether unavailable buttons remain visible or are hidden.
These are action buttons, not selected-state tabs. data-p3d-selected does not identify the currently matching predefined/custom configuration; managed event buttons expose availability and running state, not persistent A/B/Custom selection. The configuration-status element is for an optional custom result handler, not an automatic manager error output.
What My Configuration Retains
| Visitor action / setting | Result |
|---|---|
| First successful, settled configuration edit | Captures the complete resulting configuration automatically, including when it equals an authored suggestion/default or has an empty selection list. Unsupported or uncertain operations do not create a new snapshot. |
| Apply a predefined configuration | Applies the authored setup without overwriting My configuration. Any authored excluded groups keep their current choices. |
| Restore My configuration | Applies the saved snapshot without replacing it. |
| Make another supported configuration edit | Replaces My configuration with the complete resulting setup, including edits made after applying a predefined configuration. |
| Reload with session persistence enabled | Validates saved My configuration and makes its restore action available. It does not automatically apply it or show the legacy session-restore prompt. Authored defaults or an explicit startup configuration remain separate. |
Set localStore.sessionRestore.enabled: false in the authored viewer settings | Keeps My configuration in memory only; it does not survive reload. Unavailable browser storage likewise leaves memory-only behavior. |
This is a project-scoped browser session snapshot, not a shareable hash or an account-synchronized saved configuration. Projects without the custom-placeholder event retain their existing session-restoration behavior. Do not read or write Presenter3D's local-storage records yourself; use shared configurations for links that can be opened elsewhere.
Predefined exclusions preserve whole relational groups, including their applicable custom colors/common choices. For example, the cabinet's Wine storage and Custom shelving suggestions can preserve the current finish, while My configuration restores the complete saved setup, including its saved finish. Exclusions are authored project data, not arguments to callEvent(). Individual-item exclusions and additive recipes are not supported; omission of an authored baseline item is not a general removal instruction.
Observe Application Results
Ordinary callEvent(eventId) can resolve undefined both when an action is skipped and when a configuration-application error is caught. Do not display a success message merely because the promise resolved. Managed buttons use this ordinary event path; they do not return a receipt to merchant code.
When a custom interaction needs a result message, use the receipt form instead of attaching a second handler to a managed button. Run the following inside your own user-action handler, not automatically during page initialization. Use either a predefined-configuration event ID or the My configuration event ID:
const presetViewer = document.getElementById("experience-viewer");
const presetStatus = document.getElementById("configuration-status");
try {
if (typeof presetViewer?.callEvent !== "function") {
throw new Error("The experience is not ready for this action.");
}
const receipt = await presetViewer.callEvent("YOUR_CONFIGURATION_EVENT_ID", {
runWhenOffscreen: true,
reportResult: true,
requestId: `configuration-${Date.now()}`,
});
presetStatus.textContent = receipt?.status === "completed" && receipt.completionConfirmed
? "Configuration applied."
: "The configuration change could not be confirmed.";
} catch (error) {
presetStatus.textContent = error instanceof Error ? error.message : "The configuration request failed.";
}
Use the custom-handler readiness checks to drive your own button state and recheck at click time. The call retains safe scenario/availability guards; no force option is needed. A loaded offscreen viewer can resume, but this call does not start an unloaded viewer.
Supply a nonempty requestId of at most 128 characters. Event receipts distinguish completed, unavailable, canceled, failed and unconfirmed; caught application errors produce failed with completionConfirmed: false. Keep try/catch for API validation and other propagated exceptions. Do not parse diagnostic error messages as stable error codes or treat cancellation/unconfirmed completion as rollback.
Configuration-preset events support receipts; direct object/material/color preset selections and configuration-tray opening events are different paths and do not. Application validates the requested configuration before resetting/applying it, but a failure after scene application begins has no rollback guarantee. Missing, incompatible or unsupported choices cannot be assumed restorable just because a button is available. Object/material/color restoration is supported; explicit texture selections and nonempty saved variable values are not.
Studio owns the definitions in swaps.predefinedStates and the matching demo events. No separate public preset-authoring/capture API or dashboard preset editor is provided by this feature. Detailed event-side-effect constraints belong to project authoring, not storefront integration.
Before publishing an integration, test both suggestions, custom capture/restoration, persistence on/off, reload, fullscreen, multiple viewers, and actual target browsers including iOS. Controlled smoke coverage alone does not establish actual WebGL/fullscreen/reload compatibility for this feature.
Save or Apply a Hash
On a connected, ready, idle, server-backed viewer:
const saved = await viewer.createSharedConfiguration();
// saved.configId is the configuration hash; saved.url is a standalone URL.
await viewer.restoreSharedConfiguration(saved.configId);
The second call validates and applies the saved setup to the current viewer. To restore through startup/reload instead:
const hash = new URL(window.location.href).searchParams.get("configId");
if (hash !== null) {
if (!/^[a-f0-9]{16}$/.test(hash)) throw new Error("Invalid configuration hash.");
viewer.setAttribute("config-id", hash);
}
Set the initial attribute before calling start(); changing it on a running tag schedules a reload. The merchant page's query parameters are not automatically forwarded to unrelated tags. For your own share URL, attach saved.configId to the URL of the page hosting the matching project rather than using saved.url.
config-id remains an explicit startup instruction on later reloads. Remove it when it should no longer be used; removing it on an initialized tag also schedules a reload. Do not seed Presenter3D's localStorage records manually.
Shared records are project-scoped and content-addressed. They exclude prices, stock, market, camera, and personal data. A hash is not an access credential. Project access and origin restrictions still apply, including when opening a standalone share URL.
Restoration is all-or-nothing: missing, disabled, withdrawn, or incompatible choices reject the whole setup. Out-of-stock status can prevent ordering but does not itself prevent restoration. Explicit texture selections and nonempty saved variable values are not supported by this restoration path. A rejected live restore leaves the current configuration unchanged before application; a failed startup restore shows a notice and loads authored defaults rather than an unrelated local session. Always handle rejected promises; this is not rollback of arbitrary later runtime failures.
The global equivalents are api.createSharedConfiguration({ projectId }) and api.restoreSharedConfiguration({ projectId, configId }). Flat-only media cannot create a 3D configuration snapshot. Local editor projects, historical previews, and agent-token sessions are outside these browser sharing APIs.
Both return Promise<SharedConfiguration>: { schemaVersion: 1, projectId, configId, configuration, createdAt, url }. configuration is the normalized saved setup; createdAt is the server timestamp. Saving is on demand, not on every selection change.
When native commerce is registered, shared requests reuse that authorization as well as project access. An invalid registered token is not silently replaced by anonymous access.
Generate the Authored PDF
Check PDF availability in the project's integration reference before adding a PDF action. The project must have an authored configuration-document definition; the API does not create one. Generation requires a connected, ready, idle viewer and does not automatically download a file:
const pdf = await viewer.generateConfigurationPdf({
includeShareLink: { configHash: true },
});
if (!pdf.valid || !("blob" in pdf) || !pdf.blob) {
throw new Error("The project document could not be generated.");
}
// pdf.blob is the generated PDF. Your page decides how to download or preview it.
// pdf.sharedConfiguration, when requested, includes configId and url.
includeShareLink accepts true for the standalone URL, { fullURL: true }, { configHash: true }, or both flags. false disables sharing for this generation. Omission uses the authored setting; an explicit object replaces it rather than inheriting omitted flags. Hash-only printing does not revoke standalone access. Failure to create a requested share record fails PDF generation rather than silently omitting it.
Listen on window for configurationPdfProgress before generating. Filter detail.projectId and detail.instanceId. Its progress is 0..100; status is started, progress, completed, or failed. It also covers the native tray PDF action and is separate from viewer loading. Always handle the API result/rejection: preflight failures may occur before progress begins, and completed means PDF creation, not download completion. Remove your listener during teardown.
Languages
The project's integration reference distinguishes two levels of language support:
| Level | What is translated |
|---|---|
| Native UI support | Presenter3D's built-in controls, dialogs and system messages. This does not establish that a particular experience is fully translated. |
| Full experience support | Native UI plus the experience's own dictionary for event names, configurable-item labels and other project-specific user-facing content. |
For a fully localized experience, select a language listed for full experience support. Selecting a native-UI-only language does not generate the missing project dictionary; project-specific text can remain in its authored language. Merchant-owned page text is localized separately.
Set the initial lang attribute to the chosen two-letter language code. To switch at runtime:
| Method / argument | Options and return |
|---|---|
api.setLanguage(language?, options?) |
Two-letter string; null/undefined clears the explicit override. |
options.surfaceScope |
"all" (default) or "commerce-driven". |
options.preserveConfiguration |
Boolean; false/omitted performs a normal synchronous update returning undefined. True returns Promise<SharedConfiguration> and requires explicit language and projectId. |
options.projectId |
Configuration to preserve, not a language-update scope. |
api.registerCommerceDrivenSurfaces(targets) |
One { projectId, instanceId? } object or an array; replaces the registered scope. Empty array clears it. Returns void. No token required. |
api.setLanguage("fr");
api.setLanguage(null); // Clear the explicit language.
await api.setLanguage("nl", {
preserveConfiguration: true,
projectId,
});
The preserving call saves a shared configuration before setting the hash and language in one batched reload. It requires a ready, idle, server-backed viewer and an explicit two-letter destination language. Handle save failures or conflicts without pretending the switch succeeded. The returned promise resolves with the saved record after scheduling the reload, not after 3D restoration finishes. Ordinary calls remain synchronous. The current market is unchanged.
Language changes default to all surfaces on the page. projectId identifies the configuration to preserve, not the language scope; other projects follow their normal restoration policy. The instance equivalent also requires that explicit project ID. For scoped language updates, register targets with api.registerCommerceDrivenSurfaces([{ projectId, instanceId }]) and pass surfaceScope: "commerce-driven". The historical scope name does not require native commerce registration.
Use the published language lists from the project's integration reference rather than assuming every native UI language is fully supported by that experience. Switching language neither creates missing translations nor changes prices or markets. Language selection itself does not require Commerce Engine registration.
Multiple Surfaces, Attributes, and Lifecycle
Use at most one non-flat 3D viewer for each project on a page. Additional same-project appearances should be flat and have distinct data-p3d-instance-id values. Media registrations may be project-wide or instance-specific; selection and product quantity are shared by project. Different projects remain independent.
Use presenter3d.getByProjectId(projectId, instanceId?) or a specific DOM element. Do not use getFirst() on multi-experience pages when you mean a particular project.
| Attribute | Meaning/default |
|---|---|
project-id |
Required published project identity. |
data-p3d-instance-id |
Optional stable surface identity; one is generated when omitted. |
flat |
Permanent 2D surface; defaults to false. |
wait |
Participates in delayed 3D startup; defaults to false. |
lang |
Explicit two-letter language; omitted uses normal language resolution. |
config-id |
Explicit shared-configuration startup instruction. |
loading-bar, updating-bar, poster |
Native download bar, local-processing bar, and poster; enabled by default. |
show-preview |
Preview loading presentation; disabled by default. |
pause-audio-on-start |
Start with audio paused; disabled by default. |
no-auto-video |
Suppress automatic passive video scheduling, not manually invoked events; disabled by default. |
no-analytics |
Suppress analytics; disabled by default. Set before startup when required. |
disconnect-dispose-delay |
Optional nonnegative milliseconds before removal disposes a tag, useful for layouts that briefly detach it. |
For these boolean attributes, presence enables the option and the exact string "false" disables it. Do not use strings such as "0" as false. Runtime defaults on initial markup and removing an observed attribute later are not necessarily identical; prefer explicit "true"/"false" for runtime changes. Some changes reload the viewer. Use the documented language/market methods rather than coordinating reload attributes yourself.
For SSR frameworks, render static markup on the server and access window, document, and custom-element APIs only in client mount hooks. Wait for the target controls as well as the custom-element definition. Cancel pending setup if the component unmounts first.
Global and Instance Methods
These helpers are on window.presenter3d, not on api:
| Method | Arguments / return |
|---|---|
getByProjectId(projectId, instanceId?) |
Returns the matching tag or undefined. Prefer this or a specific DOM element on multi-project pages. |
getFirst() |
First registered primary viewer or undefined; not a project selector. |
applyToAll(callback) |
Calls (viewer) => void for each registered primary viewer; no aggregate promise. |
list() |
Logs primary project IDs; does not return an array. |
startAll() |
Releases passive startup and invokes primary viewers' start(); its promise is not collective scene readiness. |
observe() / init() |
Legacy page-wide initialization entry points; promises are not readiness signals. Prefer per-tag start() in new integrations. |
runPassive(options?) |
Configure passive startup before it starts; returns the function that releases it. Boolean true/default enables deferred core startup; false disables deferral. Object: core?: boolean | { onPageDone?: boolean, delayMs?: number }, root onPageDone?, delayMs?, commerce?: boolean | "early" | "defer" | { network?: boolean | "early" | "defer" }. Commerce true/"defer" defers networking; false/"early" does not. Enabled-core defaults: page load plus 1200ms; commerce defaults early. Cannot unload already-started code. |
disposeAll() |
Promise disposing every registered viewer/surface. Not component-level cleanup. |
disposeProvider() |
Promise disposing the page-wide provider and its viewers. Not component-level cleanup. |
denyIdentityToken() |
Disables identity-token usage on currently registered viewers whose API exists; not a future-tag or analytics-off policy. |
viewer.start() takes no arguments and returns a promise; observe readiness separately. viewer.setCountdownVisible(visible?) accepts true, false or no argument to restore the project default. Hybrid media still suppresses it.
Instance shortcuts use viewer.commerce. They default the project, but do not rename global argument positions:
| Instance call | Difference from global api |
|---|---|
registerVariations(registrationOrArray, projectId?) |
Registration first; global takes project first. |
getVariations(projectId?, instanceId?) |
Defaults both to this tag. |
selectVariation(variantId, projectId?) |
Variation first. |
callVariation(variantId, projectId?, options?) |
Options are third; use undefined as the second argument to retain this project. |
getSelectedVariation(projectId?) |
Defaults to this project. |
registerConfigurationTray(options) |
No project field; also available as viewer.registerConfigurationTray(options). |
viewerProgressManager, externalUIManager, subscribeLiveTagContent, reflectBusinessData |
Same option objects with optional project; external-controls/live-text also default instance ID. |
createSharedConfiguration() / restoreSharedConfiguration(configId) |
No project argument; also available directly on the tag. |
setLanguage, setMarket, registerCommerceDrivenSurfaces |
Still page-wide/scoped operations, not automatically limited to this tag. Preservation still needs explicit projectId. |
There is no instance unregisterVariations or setPauseOnHostScroll; use the global methods. Native-commerce shortcuts and their argument differences are covered in the commerce guide.
Dispose progress, external-control, tray, quantity, and business-reflection bindings in their owning component. Remove your event listeners and timers. Unregister instance media with api.unregisterVariations(projectId, instanceId) when that registration is no longer needed; do not remove project-wide media still used elsewhere. Removing a viewer invokes its lifecycle cleanup. Do not call disposeProvider() to tear down just one product: it affects the whole page.
Access, Analytics, and Deployment
The customer manages experience password/access-code protection and permitted origins in the dashboard. These are separate from the integration-info password and Commerce Engine tokens. The server may reject an unapproved origin or request a project access code. Test the real production scheme, domain, subdomain, and port, as well as any approved staging origin. Keep access prompts usable; neither a share hash nor access to integration metadata bypasses them.
Do not publish a project password in HTML or JavaScript to avoid the prompt. Merchant-hosted gallery images have their own access policy and are not protected automatically by viewer restrictions. Published Experience Management changes can affect globally available events, variations, and languages without requiring a commerce token on each embedding page. Administrative changes are not public browser APIs.
no-analytics suppresses analytics, including supported startup-failure reporting. Set it before initializing the experience when your page requires that policy. viewer.allowIdentityToken(false) is a narrower identity-token choice, not an analytics-off switch, and is only callable once that viewer API exists. presenter3d.denyIdentityToken() applies to currently registered viewers with that API; it is not a guaranteed pre-startup consent setting for future tags. Keep identity choices and analytics suppression distinct.
For deployment, preserve the supplied loader and compatible release selection. Configure your site's CSP for its scripts/styles, project/resource requests, images/media, and workers as applicable. Use approved nonces or hashes for inline integration code where required; do not broadly disable CSP. Image access/CORS requirements can differ between displaying a photo and using it in generated documents. Check failed requests in the browser on the actual merchant origin.
Keep controls keyboard accessible, label icon-only buttons, and provide textual status outside graphical progress. Do not make essential operations hover-only. Test narrow layouts and fullscreen; fullscreen behavior depends on browser support and user gestures.
Events and Analytics
Listen on window and filter detail.projectId; filter instance ID where supplied. Remove listeners on unmount.
| Public event | Detail / purpose |
|---|---|
viewerUnavailable |
Bubbling tag event { projectId, instanceId?, reason, message }; raw events are not replayed. Prefer the progress manager for late subscribers. |
configurationPdfProgress |
PDF lifecycle described above; separate from loading progress. |
businessDataSummary |
{ projectId, reason, summary, ... }; use reflectBusinessData for routine price UI. |
swapConfig |
Current saved-configuration shape with projectId, selections, optional region/common selections/custom colors. Serialized variationGroupId is not the callEvent option name (relationalId). |
eventInterrupted |
{ projectId?, eventId, isVideoMode? }; an interrupted cancellable event or stopped video mode. Not a completion event. |
viewer.setAnalytics(options) is synchronous when installed. Optional fields: referrer: string, referrerPath: string[], initialSwappingState: SavedConfig, hostEventId: { eventId, customData? }, runtimeContext: string[], reloaded: { from?, reason?: Record<string, string | number | boolean | null> }. Use only merchant-owned context, not secrets or buyer credentials. It does not override no-analytics.
viewer.setAnalytics?.({ hostEventId: { eventId: "merchant_details_opened", customData: { source: "product-page" } } });
viewer.allowIdentityToken?.(false); // Identity choice only; use no-analytics for suppression.
For explicitly enabled agent integrations, await viewer.getAgentData() takes no arguments and fetches authorized project metadata after the server-backed scene is ready. Result: { projectId, source, agentRevision, sourceRecordId, sourceRecordVersion, uploadSessionId, data }. It does not start an agent or expose an MCP endpoint. Ordinary merchant buttons do not need it; use supplied IDs and the browser methods above.
External Tray Recipe
An optional complete example combining the APIs above. Keep the generated viewer, static media and loader from the quick start; replace its controls and setup module with this example, rather than running both.
Project and Page Requirements
Use the project's dashboard integration reference or getIntegrationInfo() to obtain:
- The configuration-opening event ID from
integrationInfo.events, and its flow ID from that action'sconfigurationFlowId. In bar these are1790724526599andconf. - Whole branch IDs from
integrationInfo.trayBranches[flowId], such as bar'sfinishes, for tray exclusions. - Finish variation IDs and replacement-thumbnail preset keys from
integrationInfo.presets. For example,color:vc_01supplies raw variation IDvc_01;object:vo_1is already the fullpresetVisualskey.
This recipe needs no node/entry IDs. Those are not a structured part of the response; optional node-specific visuals or preset prerequisites require IDs supplied separately by Studio or in explicit descriptions/public customer notes. Only whole branches can be excluded. The native tray owns configuration rules; do not derive IDs or rules from labels, SKUs or DOM text.
Replace the example IDs with those in your project's catalog. YOUR_CONFIGURATION_EVENT_ID must open the flow named by YOUR_CONFIGURATION_FLOW_ID. The project must have capabilities.pdf: true for the PDF button. Its initial finish must match the first image/registration unless a restored setup or explicit visitor choice takes precedence.
Use a deployed runtime containing the documented tray binding/state methods. If feature detection below fails, update the supplied deployment rather than implementing a private-DOM workaround.
Keep the generated viewer/static image/poster framing and loader inside a merchant-owned wrapper with id="experience-mount". This wrapper must contain only this experience; the runtime may insert its gallery wrapper between it and the viewer. Give the viewer id="experience-viewer", data-p3d-instance-id="main-experience", its real project-id, and the appropriate startup policy. Mount this content outside the viewer. If adapting the introduction, replace its controls/status and setup script rather than installing both scripts.
Merchant Markup
<div id="experience-controls">
<button type="button" data-p3d-variation-id="vc_01">First finish</button>
<button type="button" data-p3d-variation-id="vc_02">Second finish</button>
<button type="button" data-p3d-native-action="isFullscreen">Fullscreen</button>
</div>
<p><span id="base-price"></span> <span id="configured-price">Price unavailable</span></p>
<div id="product-actions">
<button id="configure" type="button" disabled aria-expanded="false"
aria-controls="configuration-panel">Configure</button>
<button id="generate-pdf" type="button" disabled>Generate PDF</button>
</div>
<p id="experience-status" role="status">Loading interactive view...</p>
<p id="action-status" role="status"></p>
<progress id="pdf-progress" max="100" value="0" hidden aria-label="PDF generation"></progress>
<div id="configuration-panel"></div>
<p>Your product description stays in normal page flow below the tray.</p>
<style>
#configuration-panel { width: 100%; min-width: 0; border-top: 1px solid #aaaaaa; }
#configuration-panel[data-expanded="true"] { border-top-color: transparent; }
#experience-controls .p3d-is-selected { outline: 2px solid currentColor; }
#experience-controls [aria-disabled="true"] { opacity: 0.5; }
#experience-status, #action-status { overflow-wrap: anywhere; }
#experience-mount[data-merchant-failure-notice="true"] [data-p3d-viewer-unavailable-notice] {
display: none;
}
</style>
Configure/PDF are custom buttons: their disabled state belongs to this script. Do not put data-p3d-event-id on Configure as well, or the setup event may run twice. Managed finish/fullscreen controls have no literal disabled attribute.
The failure selector is inactive until the script has displayed an accessible replacement. The description and tray share normal flow; the runtime owns tray height animation. Do not animate the wrapper height separately or hide the viewer while waiting for readiness.
Browser Setup
Run this as the page’s single client module after the markup. It deliberately does not call api.register() or getProductOverview(). Reflected pricing depends on data already supplied by the viewer; without such data it keeps the placeholder.
Use the configuration-opening event and its matching flow ID from your project's reference. This recipe confirms that flow through tray.subscribe(), not through the return timing of callEvent(). Its 15-second timeout is a page-level error policy, not event cancellation.
Recovery policy: after a failed/unconfirmed opening, this recipe keeps that flow collapsed until the user retries the external Configure button. The subscription cannot distinguish a late opening from a deliberate native request, so it also suppresses native openings in fullscreen during this failure state. Exit fullscreen and retry Configure to recover. Outside that failure state, native/fullscreen synchronization works normally. Do not treat a timeout as cancellation or claim that this policy preserves native retries.
Custom Configure/PDF actions remain disabled until their handlers and bindings are installed. If setup or viewer.start() rejects, they stay disabled until page reload/remount; already initialized media and managed controls are retained for the flat fallback. The reload instruction stays in actionStatus, independently of later loading/readiness messages in status.
Runtime compatibility: after api.registerConfigurationTray(...), the recipe checks that the returned handle has callable getState, subscribe, update, and dispose methods. Custom-element registration alone does not prove that an older release supports this contract. This capability check is independent of scene readiness; do not remove it or substitute empty methods to enable the buttons.
If that check fails, regenerate the embed from the project's dashboard Integration section and replace the old loader/bootstrap with the generated version. Remove merchant-added hardcoded bundle URLs or release overrides rather than loading a second runtime beside the first. If the generated loader still selects an incompatible project release, contact Presenter3D with the project ID and the failing method check to arrange a compatible release. Then reload the full page and rerun the check. Reloading an unchanged pinned deployment cannot add missing methods; keep the image fallback and custom actions disabled until compatibility is resolved.
const viewer = document.getElementById("experience-viewer");
const experienceMount = document.getElementById("experience-mount");
const projectId = viewer.getAttribute("project-id");
const instanceId = viewer.dataset.p3dInstanceId;
const setupEventId = "YOUR_CONFIGURATION_EVENT_ID";
const setupFlowId = "YOUR_CONFIGURATION_FLOW_ID";
const setupButton = document.getElementById("configure");
const pdfButton = document.getElementById("generate-pdf");
const trayHost = document.getElementById("configuration-panel");
const status = document.getElementById("experience-status");
const actionStatus = document.getElementById("action-status");
const pdfProgress = document.getElementById("pdf-progress");
let api, tray, controls, prices, progress, unsubscribeTray;
let actionsReady = false;
let disposed = false, togglePending = false, pdfPending = false, pdfRunning = false;
let stateTimer, downloadTimer, downloadUrl;
let cancelTrayOpen, suppressFailedOpen = false;
const trayOptions = {
containerElementId: "configuration-panel",
visible: false, visibilityPolicy: "shared",
heightMode: "fixed", maxHeight: 400, expansionDurationMs: 300,
collapsedHeight: 0, optionsWindowBehavior: "keepOpen",
panelCorners: { topLeft: 8, topRight: 8, bottomLeft: 8, bottomRight: 8 },
hiddenControls: ["close", "download"],
settings: {
heightCapacity: 2,
style: { fontFamily: "Arial, sans-serif", accentColor: "#527b70" },
panelStyle: { backgroundColor: "#f7f7f7", textColor: "#242424",
mutedTextColor: "#646464", shadow: false, borderColor: "transparent" },
},
nativeExclusions: [{ eventId: setupEventId, target: "sidebar", restoreOnFullscreen: true }],
excludedBranches: [{ flowId: setupFlowId, branchId: "finishes", restoreOnFullscreen: true }],
presetVisuals: { "object:vo_1": { imageUrl: "/images/classic-handle.webp" } },
};
function syncControls() {
if (disposed) return;
if (!actionsReady || typeof tray?.getState !== "function" || typeof tray?.subscribe !== "function") {
setupButton.disabled = true;
pdfButton.disabled = true;
return;
}
const state = viewer.getExternalUIState?.({ runWhenOffscreen: true });
const blocked = !state?.runtime.ready || state.runtime.loading || state.runtime.busy ||
!!state.pendingDialog || togglePending || pdfPending || pdfRunning;
const currentTray = tray.getState();
const retainedFlow = currentTray?.open && currentTray.flowId === setupFlowId;
setupButton.disabled = blocked || !tray || typeof viewer.callEvent !== "function" ||
(!retainedFlow && !state?.eventIds.includes(setupEventId));
// PDF requires the ordinary ready state, not only resumable offscreen readiness.
pdfButton.disabled = blocked || !viewer.getExternalUIState?.().runtime.ready;
}
async function toggleTray() {
if (disposed || togglePending || !actionsReady) return;
syncControls();
if (setupButton.disabled) return;
const before = tray.getState();
const opening = !before.visible;
const needsOpenEvent = opening && (!before.open || before.flowId !== setupFlowId);
if (opening) suppressFailedOpen = false;
togglePending = true;
syncControls();
actionStatus.textContent = "";
try {
tray.update({ ...trayOptions, visible: opening });
if (needsOpenEvent) {
await new Promise((resolve, reject) => {
let finished = false, callResolved = false, flowOpen = false;
let unsubscribe = () => {};
const finish = error => {
if (finished) return;
finished = true;
clearTimeout(timer);
unsubscribe();
cancelTrayOpen = undefined;
if (error) reject(error); else resolve();
};
const timer = setTimeout(() => finish(new Error(
"The configuration flow did not open in time. Please try again."
)), 15000);
cancelTrayOpen = () => finish(new Error("Configuration opening was canceled by page teardown."));
try {
// Subscribe before dispatch; subscribe also calls back immediately.
unsubscribe = tray.subscribe(state => {
flowOpen = state.open && state.flowId === setupFlowId;
if (flowOpen && callResolved) finish();
});
if (finished) unsubscribe();
Promise.resolve().then(() => {
if (disposed || finished) return;
return viewer.callEvent(setupEventId, { runWhenOffscreen: true });
}).then(() => {
callResolved = true;
if (flowOpen) finish();
}, error => finish(error));
} catch (error) {
finish(error);
}
});
}
} catch (error) {
if (!disposed) {
suppressFailedOpen = needsOpenEvent;
tray.update({ ...trayOptions, visible: false });
actionStatus.textContent = error instanceof Error ? error.message : "Could not change the tray.";
if (suppressFailedOpen) actionStatus.textContent += " Exit fullscreen if necessary, then retry Configure.";
}
} finally {
togglePending = false;
syncControls();
}
}
function onPdfProgress(event) {
const detail = event.detail;
if (disposed || detail.projectId !== projectId || detail.instanceId !== instanceId) return;
pdfRunning = detail.status === "started" || detail.status === "progress";
pdfProgress.hidden = !pdfRunning;
pdfProgress.value = detail.progress;
syncControls();
}
async function generatePdf() {
syncControls();
if (disposed || pdfButton.disabled) return;
pdfPending = true;
syncControls();
actionStatus.textContent = "Preparing PDF...";
try {
const result = await viewer.generateConfigurationPdf({ includeShareLink: { configHash: true } });
if (disposed) return;
if (!result.valid || !("blob" in result) || !result.blob) {
throw new Error("The project document could not be generated.");
}
clearTimeout(downloadTimer);
if (downloadUrl) URL.revokeObjectURL(downloadUrl);
downloadUrl = URL.createObjectURL(result.blob);
const link = document.createElement("a");
link.href = downloadUrl;
link.download = "configuration.pdf";
document.body.appendChild(link);
link.click();
link.remove();
downloadTimer = setTimeout(() => {
URL.revokeObjectURL(downloadUrl);
downloadUrl = undefined;
}, 60000);
actionStatus.textContent = "PDF created.";
} catch (error) {
if (!disposed) actionStatus.textContent = error instanceof Error ? error.message : "PDF generation failed.";
} finally {
pdfPending = false;
if (!disposed) { pdfProgress.hidden = true; syncControls(); }
}
}
function disposeRecipe() {
if (disposed) return;
disposed = true;
cancelTrayOpen?.();
clearInterval(stateTimer);
clearTimeout(slowNotice);
clearTimeout(downloadTimer);
if (downloadUrl) URL.revokeObjectURL(downloadUrl);
setupButton.removeEventListener("click", toggleTray);
pdfButton.removeEventListener("click", generatePdf);
window.removeEventListener("configurationPdfProgress", onPdfProgress);
window.removeEventListener("pagehide", onPageHide);
unsubscribeTray?.();
tray?.dispose?.();
controls?.dispose();
prices?.dispose();
progress?.dispose();
api?.unregisterVariations(projectId, instanceId);
// This recipe owns the page-wide policy. Shared applications must coordinate it.
api?.setPauseOnHostScroll(false);
experienceMount.removeAttribute("data-merchant-failure-notice");
setupButton.disabled = true;
pdfButton.disabled = true;
}
function onPageHide(event) { if (!event.persisted) disposeRecipe(); }
window.addEventListener("pagehide", onPageHide);
const slowNotice = setTimeout(() => {
if (!disposed) status.textContent = "The interactive view is taking longer to load.";
}, 15000);
try {
await customElements.whenDefined("presenter-3d");
if (!disposed && viewer.isConnected) {
clearTimeout(slowNotice);
api = window.presenter3d.commerce;
// These are ordinary photos. For exported posters use variantImageFraming: "poster"
// on each entry and retain the generated wrappers/CSS/data-p3d-poster-framing JSON.
api.registerVariations(projectId, {
instanceId, behavior: "slider", viewerBehaviorOnSlider: "focus",
variations: [
{ variantId: "vc_01", label: "First finish", variantImageUrl: "/images/variant-one.webp", variantImageFraming: "standard" },
{ variantId: "vc_02", label: "Second finish", variantImageUrl: "/images/variant-two.webp", variantImageFraming: "standard" },
],
});
controls = api.externalUIManager({ projectId, instanceId, containerElementId: "experience-controls" });
tray = api.registerConfigurationTray({ projectId, ...trayOptions });
if (typeof tray?.getState !== "function" || typeof tray?.subscribe !== "function" ||
typeof tray?.update !== "function" || typeof tray?.dispose !== "function") {
throw new Error("This runtime lacks tray-state subscriptions. Update the generated embed; contact Presenter3D if it remains incompatible.");
}
unsubscribeTray = tray.subscribe(state => {
if (suppressFailedOpen && state.open && state.flowId === setupFlowId && state.visible) {
// This also suppresses fresh native requests; exit fullscreen and retry Configure.
tray.update({ ...trayOptions, visible: false });
return;
}
setupButton.textContent = state.visible ? "Close settings" : "Configure";
setupButton.setAttribute("aria-expanded", String(state.visible));
trayHost.dataset.expanded = String(state.visible && state.destination === "external");
syncControls();
});
prices = api.reflectBusinessData({
projectId, basePriceElementId: "base-price",
basePriceConfig: { crossedOut: true, showOnlyOnDiscount: true },
effectivePriceElementId: "configured-price",
});
progress = api.viewerProgressManager({
projectId,
onLoadingStart() {
experienceMount.removeAttribute("data-merchant-failure-notice");
status.textContent = "Loading interactive view...";
syncControls();
},
onProgress({ progress }) { status.textContent = `Loading: ${progress}%`; },
onViewerIsReady() {
experienceMount.removeAttribute("data-merchant-failure-notice");
status.textContent = "";
const summary = viewer.getBusinessDataSummary?.();
if (summary) prices.refresh(summary);
syncControls();
},
onViewerUnavailable({ message }) {
status.textContent = message; // Visible text exists before the native notice is suppressed.
experienceMount.setAttribute("data-merchant-failure-notice", "true");
syncControls();
},
});
api.setPauseOnHostScroll(true); // Pause events and audio too, with no exemptions.
// Optional design choice: keep viewers running during events or active audio:
// api.setPauseOnHostScroll(true, { noPauseOn: ["eventRuns", "activeAudio"] });
setupButton.addEventListener("click", toggleTray);
pdfButton.addEventListener("click", generatePdf);
window.addEventListener("configurationPdfProgress", onPdfProgress);
// Tray subscription tracks tray state; polling also tracks transient runtime busy/dialog state.
stateTimer = setInterval(syncControls, 250);
actionsReady = true;
syncControls();
await viewer.start();
}
} catch (error) {
actionsReady = false;
if (!disposed) {
status.textContent = error instanceof Error ? error.message : "Integration could not start.";
actionStatus.textContent = "Configuration controls could not start. Reload the page to try again.";
}
// Keep initialized media and controls alive; dispose only on unmount/page teardown.
syncControls();
}
For an SPA, call disposeRecipe() from the component’s unmount hook, cancel pending setup, and create a new component instance on remount. Keep bindings when the browser stores the page in its back/forward cache, as above. Do not call page-wide disposeProvider() for one component.
Recipe Acceptance Checks
- Open, navigate, hide and reopen: selections and route remain; the setup event is not replayed for a retained flow.
- Enter/exit fullscreen with both visible and hidden trays; use native close/open and confirm button label/ARIA follows.
- Open a linked options window, hide the external tray, close the window: it stays hidden.
- Test two-row sizing at narrow widths and with large thumbnails/header text; extra rows scroll within the ceiling. Enable reduced motion.
- Scroll until controls are visible but the viewer is offscreen: setup/tray actions work on a loaded resumable scene without a simulated viewer click.
- Generate a PDF, test failure and repeated clicks, and verify project/instance filtering. Loading progress and PDF progress remain separate.
- Test absent WebGL2: one visible warning, usable media, no forever-loading message and no invented price.
- Reload with a valid shared setup or local restoration: do not overwrite its finish with an unconditional initial selection.
- Unmount/remount and navigate back: no duplicate listeners, stale timers or leaked tray bindings.
- Delay tray opening until after
callEvent()resolves; it must still open successfully. Reject or time out opening: the tray stays collapsed, late notifications do not reveal it, and retry works. Unmount while waiting: no remaining opening subscription or timer. - Simulate missing tray-state methods and a setup/start rejection: no second exception, Configure/PDF remain disabled even after readiness notifications, the reload instruction remains visible in
actionStatus, and initialized media stays usable. After an opening timeout, test the documented exit-fullscreen/external-retry recovery path.
Integration Checklist
For developers and AI coding agents:
- Use project/event/variation/preset/group/slot IDs from the integration reference. Choose page-specific instance IDs yourself when needed. Never derive project identifiers from labels or assume example IDs exist.
- Retain the generated loader and poster-framing structure; do not invent an API namespace or call runtime-internal methods.
- Mount DOM targets before binding. Separate API availability from 3D readiness and idle-state requirements.
- Keep static media visible; handle both successful readiness and terminal startup failure.
- Leave native access, restoration, crash-recovery, and optimization dialogs accessible during loading.
- Let managed controls own their dynamic state; prevent duplicate bindings on rerenders.
- Handle promise rejection for sharing, restoration, PDF generation, and preserving language changes.
- Test ordinary loading, slow/offline loading, unsupported WebGL2, multiple projects, narrow layouts, offscreen controls, reloads, and SPA remounts.
- Respect project access and analytics policy. Do not expose secrets or use a shared hash as authorization.
- If native prices/stock/checkout are required, use Built-In Commerce Engine Integration. Otherwise do not add token registration or native checkout dependencies.
WooCommerce and Shopify integration documentation: coming soon.