Tour postMessage SDK
The Treedis tour exposes a postMessage-based SDK that lets any parent page — whether an iframe or a popup window — communicate bi-directionally with the running tour. No relay server or custom bridge script is required.
Overview
The SDK works by passing structured messages through the browser's native window.postMessage API:
- Tour → Parent:
window.opener.postMessage(event, '*')(popup) orwindow.parent.postMessage(event, '*')(iframe) - Parent → Tour:
tourWindow.postMessage(command, '*')(popup) oriframe.contentWindow.postMessage(command, '*')(iframe)
The tour auto-detects whether it is running inside an iframe or was opened via window.open() and routes messages to the correct target automatically.
Integration Patterns
Popup Window (recommended)
Open the tour in a separate browser window. The viewer and tour run side by side.
// Open the tour — must be triggered by a user gesture (click)
const tourWindow = window.open('https://app.treedis.com/tour/<model-id>', 'treedis-tour');
// Listen for events from the tour
window.addEventListener('message', (event) => {
const { type, ...data } = event.data;
switch (type) {
case 'TourReady': handleTourReady(); break;
case 'PoseChanged': handlePose(data); break;
case 'SweepsChanged': handleSweeps(data.sweeps); break;
case 'TagClicked': handleTagClicked(data.tag); break;
case 'TagHovered': handleTagHovered(data.tag); break;
case 'TagFocused': handleTagFocused(data.tag); break;
case 'TagDocked': handleTagDocked(data.tag); break;
}
});
// Send a command to the tour
function navigateToSweep(sweepId) {
tourWindow.postMessage({ type: 'Navigate', sweepId, transitionTime: 1500 }, '*');
}
Iframe Embed
Embed the tour directly in your page using an <iframe>.
<iframe
id="tour-frame"
src="https://app.treedis.com/tour/<model-id>"
allow="fullscreen"
style="width:100%;height:600px;border:none;"
/>
const frame = document.getElementById('tour-frame');
// Listen for events from the tour
window.addEventListener('message', (event) => {
const { type, ...data } = event.data;
// same handler as popup pattern above
});
// Send a command to the tour
function navigateToSweep(sweepId) {
frame.contentWindow.postMessage({ type: 'Navigate', sweepId, transitionTime: 1500 }, '*');
}
Note: Both patterns share the same event/command protocol. Only the method of opening the tour and sending commands differs.
Handshake / Connection Flow
Parent Tour
│ │
│── window.open() / <iframe src="..."> ────►│
│ │ (loading...)
│◄─── { type: 'TourReady' } ───────────────│ app state = 'playing'
│ │
│◄─── { type: 'SweepsChanged', sweeps: []}─│ sweeps loaded
│ │
│◄─── { type: 'PoseChanged', ... } ────────│ camera moved (~10Hz)
│ │
│──── { type: 'Navigate', sweepId } ───────►│
│ │
Tip: Send a Ping command after opening the tour — if the tour is already loaded, it responds immediately with TourReady. This handles the case where the tour window was already open before your page loaded.
// Ping the tour to check if it's already ready
tourWindow.postMessage({ type: 'Ping' }, '*');
Inbound Events (Tour → Parent)
These events are emitted by the tour and received by your message event listener.
TourReady
Fired when the tour has fully loaded and is ready to receive commands.
{ "type": "TourReady" }
Also fired in response to a Ping command.
PoseChanged
Fired continuously as the camera moves, approximately every 100ms.
{
"type": "PoseChanged",
"x": 1.23,
"y": 0.0,
"z": -4.56,
"rotationY": 137.4,
"sweep": "abc12345-..."
}
| Field | Type | Description |
|---|---|---|
x |
number |
Camera world position X (meters) |
y |
number |
Camera world position Y (meters) |
z |
number |
Camera world position Z (meters) |
rotationY |
number |
Camera horizontal rotation (degrees, 0–360) |
sweep |
string |
ID of the currently active Matterport sweep |
SweepsChanged
Fired once after the tour loads all sweep data. Re-emitted in response to a RequestSweeps command.
{
"type": "SweepsChanged",
"sweeps": [
{ "id": "abc12345-...", "x": 1.23, "y": 0.0, "z": -4.56 },
{ "id": "def67890-...", "x": 3.10, "y": 0.0, "z": -1.20 }
]
}
| Field | Type | Description |
|---|---|---|
sweeps[].id |
string |
Matterport sweep ID |
sweeps[].x |
number |
Sweep world position X (meters) |
sweeps[].y |
number |
Sweep world position Y (meters) |
sweeps[].z |
number |
Sweep world position Z (meters) |
Only enabled sweeps are included.
TagClicked
Fired when a user clicks on a tag in the tour.
{
"type": "TagClicked",
"tag": {
"sid": "abc123",
"defaultSid": "abc123",
"id": "tag-uuid",
"title": "Room Label",
"url": "https://...",
"type": "mattertag",
"description": "...",
"position": { "x": 1.2, "y": 1.5, "z": -3.4 }
}
}
TagHovered
Fired when the cursor hovers over a tag.
{ "type": "TagHovered", "tag": { ... } }
Same tag shape as TagClicked.
TagFocused
Fired when a tag panel comes into focus (e.g., navigating to a tag's sweep).
{ "type": "TagFocused", "tag": { ... } }
TagDocked
Fired when a tag is docked into the sidebar or info panel.
{ "type": "TagDocked", "tag": { ... } }
Tag Object Shape
All tag events share the same tag object:
| Field | Type | Description |
|---|---|---|
sid |
string |
Matterport scene ID |
defaultSid |
string |
Default scene ID |
id |
string |
Treedis tag UUID |
title |
string |
Tag display name |
url |
string |
Associated URL (if any) |
type |
string |
Tag type (e.g., "mattertag") |
description |
string |
Tag description text |
position.x/y/z |
number |
Tag 3D world position (meters) |
Outbound Commands (Parent → Tour)
Send commands by calling postMessage on the tour window or iframe.
Navigate
Moves the tour camera to the specified sweep.
tourWindow.postMessage({
type: 'Navigate',
sweepId: 'abc12345-...', // required
rotation: { x: 0, y: 180 }, // optional, degrees (x = pitch, y = yaw)
transitionTime: 1500, // optional, ms (default: 1500)
}, '*');
| Field | Type | Required | Description |
|---|---|---|---|
sweepId |
string |
Yes | Target sweep ID |
rotation |
{ x: number, y: number } |
No | Camera rotation at destination |
transitionTime |
number |
No | Transition duration in milliseconds |
RequestSweeps
Asks the tour to re-emit a SweepsChanged event with the current sweep list.
tourWindow.postMessage({ type: 'RequestSweeps' }, '*');
Ping
Checks whether the tour is ready. The tour responds with a TourReady event.
tourWindow.postMessage({ type: 'Ping' }, '*');
Useful for detecting if the tour was already loaded before your page connected to it.
Complete Integration Example
class TourSDK {
constructor(tourUrl) {
this.tourUrl = tourUrl;
this.tourWindow = null;
this.ready = false;
this._pingInterval = null;
window.addEventListener('message', this._handleMessage.bind(this));
}
// Open the tour — must be called from a user gesture (click handler)
open() {
this.tourWindow = window.open(this.tourUrl, 'treedis-tour');
this.ready = false;
// Ping until TourReady is received
this._pingInterval = setInterval(() => {
if (this.tourWindow?.closed) {
clearInterval(this._pingInterval);
this.onDisconnect?.();
return;
}
if (!this.ready) {
this.tourWindow?.postMessage({ type: 'Ping' }, '*');
}
}, 2000);
}
// Send navigate command
navigateTo(sweepId, transitionTime = 1500) {
this.tourWindow?.postMessage({ type: 'Navigate', sweepId, transitionTime }, '*');
}
// Request fresh sweep list
requestSweeps() {
this.tourWindow?.postMessage({ type: 'RequestSweeps' }, '*');
}
_handleMessage(event) {
const { type, ...data } = event.data ?? {};
switch (type) {
case 'TourReady':
this.ready = true;
clearInterval(this._pingInterval);
this.onReady?.();
break;
case 'PoseChanged':
this.onPose?.(data);
break;
case 'SweepsChanged':
this.onSweeps?.(data.sweeps);
break;
case 'TagClicked':
this.onTagClicked?.(data.tag);
break;
case 'TagHovered':
this.onTagHovered?.(data.tag);
break;
case 'TagFocused':
this.onTagFocused?.(data.tag);
break;
case 'TagDocked':
this.onTagDocked?.(data.tag);
break;
}
}
}
// Usage
const sdk = new TourSDK('https://app.treedis.com/tour/<model-id>');
sdk.onReady = () => console.log('Tour is ready');
sdk.onPose = (pose) => updatePositionDot(pose.x, pose.z, pose.rotationY);
sdk.onSweeps = (sweeps) => buildMinimap(sweeps);
sdk.onTagClicked = (tag) => showTagInfo(tag);
// Triggered by a button click
document.getElementById('open-tour').addEventListener('click', () => sdk.open());
TypeScript Types
// Inbound event types
type TourEvent =
| { type: 'TourReady' }
| { type: 'PoseChanged'; x: number; y: number; z: number; rotationY: number; sweep: string }
| { type: 'SweepsChanged'; sweeps: Array<{ id: string; x: number; y: number; z: number }> }
| { type: 'TagClicked' | 'TagHovered' | 'TagFocused' | 'TagDocked'; tag: TourTag };
// Outbound command types
type TourCommand =
| { type: 'Navigate'; sweepId: string; rotation?: { x: number; y: number }; transitionTime?: number }
| { type: 'RequestSweeps' }
| { type: 'Ping' };
interface TourTag {
sid: string;
defaultSid: string;
id: string;
title: string;
url: string;
type: string;
description: string;
position: { x: number; y: number; z: number };
}
Security Considerations
- Always validate
event.originin production if you know the exact tour domain:window.addEventListener('message', (event) => { if (event.origin !== 'https://app.treedis.com') return; // handle event }); - The
'*'targetOrigin used inpostMessageis acceptable when the tour URL is controlled by Treedis, but origin validation on the receiving end is recommended for sensitive integrations.
Troubleshooting
Tour window is null after window.open()
window.open()returnsnullif blocked by the browser's popup blocker. Always call it from inside a user gesture (aclickevent handler).
TourReady never fires
- Use the
Pinginterval pattern to retry until the tour acknowledges. The tour may still be loading.
Events stop arriving after closing and re-opening the tour window
- Re-add your
messagelistener after re-opening, or keep it onwindowpermanently (it persists across popup sessions).
Navigate command has no effect
- Ensure
TourReadyhas been received before sending commands. The tour SDK is not active until the app state reaches'playing'.

