API Reference

/

PostMessage Protocol

PostMessage Protocol

This is the contract the SDK is built on. Read it if you're embedding without the SDK or building your own framework binding.

All messages flow as JSON-serializable objects through window.postMessage. The host page and iframe agree on a source discriminator, a type, and the window that may send each message.

Origins

DirectionSource valueRequired target origin
Host → iframe (commands)'moshpit-sdk''https://moshpit.studio'
Viewer iframe → host'moshpit-viewer''*', sent only to window.parent
Editor iframe → host'moshpit-editor''*', sent only to window.parent

The two directions use different policies on purpose. A host knows the Moshpit iframe origin, so commands must use that exact origin. Never send commands with '*'.

Moshpit embeds may run on any host that has a valid session token. The saved Website domain is not an origin allowlist. The iframe therefore sends events to its immediate window.parent with targetOrigin: '*'. This does not broadcast the event to every ancestor or every open page. It removes the origin filter for that one parent window reference.

When receiving an event, check both event.origin === 'https://moshpit.studio' and event.source === iframe.contentWindow before trusting the payload. The SDK performs both checks for you. See Credentials and security for the session and host-domain trust model.

Message envelopes

Command (host → iframe)

TS

TypeScript

{
  source: 'moshpit-sdk';
  type: 'command';
  target: 'viewer' | 'editor';
  command: string;
  value?: unknown;       // optional payload for commands that take args
}

Event (iframe → host)

TS

TypeScript

{
  source: 'moshpit-viewer' | 'moshpit-editor';
  type: string;          // event name, e.g. 'ready', 'saved'
  payload?: unknown;     // optional event payload
}

Sending a command

JS

JavaScript

const iframe = document.getElementById('moshpit-viewer');
 
iframe.contentWindow.postMessage(
  {
    source: 'moshpit-sdk',
    type: 'command',
    target: 'viewer',
    command: 'play',
  },
  'https://moshpit.studio',
);

For commands that take a value (setQuality, goToAnnotation, openProject, updateSession):

JS

JavaScript

iframe.contentWindow.postMessage(
  {
    source: 'moshpit-sdk',
    type: 'command',
    target: 'editor',
    command: 'openProject',
    value: { splatId: '65f1a2b3c4d5e6f7a8b9c0d1' },
  },
  'https://moshpit.studio',
);

If target doesn't match the iframe (e.g. you send target: 'editor' to a viewer iframe), the message is silently ignored.

Receiving an event

JS

JavaScript

const iframe = document.getElementById('moshpit-viewer');
 
window.addEventListener('message', (event) => {
  if (event.origin !== 'https://moshpit.studio') return;
  if (event.source !== iframe.contentWindow) return;
  const data = event.data;
  if (data?.source !== 'moshpit-viewer' && data?.source !== 'moshpit-editor') {
    return;
  }
 
  console.log(data.type, data.payload);
});

Command catalog

Viewer commands (target: 'viewer')

Commandvalue shape
playundefined
pauseundefined
muteundefined
unmuteundefined
fullscreenundefined
setQuality'auto' | 'high' | 'medium' | 'low'
goToAnnotationnumber (zero-based index)
updateSession{ sessionToken: string; expiresAt?: string }

Reference: Viewer Commands.

Editor commands (target: 'editor')

Commandvalue shape
saveundefined
publishundefined
openProject{ splatId: string }
createProjectundefined
updateSession{ sessionToken: string; expiresAt?: string }

Reference: Editor Commands.

Event catalog

Viewer events (source: 'moshpit-viewer')

Eventpayload shape
readyundefined
viewTracked{ viewCount?: number; isNewView?: boolean }
fullscreenChanged{ fullscreen: boolean }
sessionExpiring{ expiresAt: string }
qualityChanged{ quality: 'auto' | 'high' | 'medium' | 'low' }
loaded{ splatId: string; source: 'gallery' | 'embed'; hasLods: boolean }
loadError{ message: string }
camera-change{ position: { x: number; y: number; z: number }; rotation: { yaw: number; pitch: number; roll: number } }
scene-change{ sourceSplatId: string; destinationSplatId: string }
cloned{ splatId: string; sourceSplatId: string }
hostFullscreenRequested{ lockLandscape?: boolean; reason?: 'mobile-landscape-interaction' }
hostFullscreenExitRequestedundefined
error{ message: string }, emitted alongside loadError for compatibility

Reference: Viewer Events.

Editor events (source: 'moshpit-editor')

Eventpayload shape
readyundefined
projectLoaded{ splatId: string }
projectCreatedundefined
saved{ splatId?: string | null }
published{ splatId: string; visibility: 'public' | 'private' }
cloned{ splatId: string; sourceSplatId: string }
dirtyChanged{ dirty: boolean }
authRequired{ action?: string; message?: string }
sessionExpiring{ expiresAt: string }
limitReached{ dimension: 'storageBytes' | 'splatCount'; scope: 'customer' | 'account'; usedBytes: number; limitBytes: number; requestedBytes?: number }
error{ message: string }

Reference: Editor Events.

When saved includes splatId, update the host route or stored project state so reopening the host page can pass that splatId back into the editor.

cloned is emitted by both the embedded viewer's Remix button and the embedded editor's Duplicate scene action after a successful clone: splatId is the new copy, and sourceSplatId is the scene it was cloned from. The embedded viewer doesn't navigate on cloned — the host decides what to do with the new scene. The embedded editor loads the new copy in place, the same way openProject does.

Always validate the sender

Check both the Moshpit event.origin and the iframe event.source. The SDK does this automatically; raw listeners must as well.

What's next