Editor Embed

/

Events

Editor Events

The editor reports lifecycle events and user actions back to the host page. Subscribe with editor.on(...) when using the SDK, or with a raw message listener when not.

With the SDK

JS

JavaScript

const editor = Moshpit.editor('#moshpit-editor', {
  publicKey: 'mpk_...',
  sessionEndpoint: '/api/moshpit/editor-session',
});
 
editor.on('ready', () => {
  // Editor finished loading
});
 
editor.on('saved', ({ splatId }) => {
  // splatId is set on first save of a new project
});
 
editor.on('dirtyChanged', ({ dirty }) => {
  saveBtn.disabled = !dirty;
});

.on(...) returns an unsubscribe function. editor.destroy() removes all listeners automatically.

Event reference

EventPayloadFires
ready
undefined

When the editor finishes loading and accepts user input.

projectLoaded
{ splatId: string }

When an existing splat finishes loading — either initial mount with `splatId`, or after `openProject(splatId)`.

projectCreated
undefined

After `createProject()` completes and the canvas is blank.

saved
{ splatId?: string | null }

When a save operation succeeds. `splatId` is included after a new scene is created so hosts can update their own route, for example `/editor?splatId=...`.

published
{ splatId: string; visibility: "public" | "private" }

When a publish operation succeeds.

cloned
{ splatId: string; sourceSplatId: string }

After the "Duplicate scene" action successfully clones the current scene. `splatId` is the new copy; `sourceSplatId` is the scene it was cloned from. The editor loads the new copy in place — the same navigation `openProject` uses — rather than reloading the iframe.

dirtyChanged
{ dirty: boolean }

When the unsaved-changes state flips. Use this to enable/disable a Save button.

authRequired
{ action?: string; message?: string }

When a guest editor session attempts a save, upload, or publish action that requires a scoped external user.

sessionExpiring
{ expiresAt: string }

About 60 seconds before the session JWT expires. The SDK uses this to refresh; intercept only if you also need to.

limitReached
{ dimension: "storageBytes" | "splatCount"; scope: "customer" | "account"; usedBytes: number; limitBytes: number; requestedBytes?: number }

When an external user is blocked by a quota. The editor shows a neutral, white-label message — no Moshpit branding or billing. scope "customer" = the per-customer allocation you set (raise it via PUT /api/v1/external-user). scope "account" = the upload would exceed your Studio account and overage is not configured (with overage on, the account accrues overage and never blocks a customer). Use this to drive your own flow.

error
{ message: string }

When the iframe encounters a recoverable error — failed save, malformed asset, etc.

Without the SDK

Listen on window, validate the origin, and check data.source === 'moshpit-editor':

JS

JavaScript

const iframe = document.getElementById('moshpit-editor');
 
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-editor') return;
 
  switch (data.type) {
    case 'ready':
      console.log('editor ready');
      break;
    case 'projectLoaded':
      console.log('loaded', data.payload.splatId);
      break;
    case 'saved':
      // First-save delivers the new splatId
      if (data.payload?.splatId) persistNewSplatId(data.payload.splatId);
      break;
    case 'published':
      console.log('published', data.payload.splatId, data.payload.visibility);
      break;
    case 'cloned':
      console.log(
        'duplicated',
        data.payload.splatId,
        data.payload.sourceSplatId,
      );
      break;
    case 'dirtyChanged':
      saveBtn.disabled = !data.payload.dirty;
      break;
    case 'authRequired':
      showSignInPrompt(data.payload?.message);
      break;
    case 'sessionExpiring':
      refreshSession();
      break;
    case 'limitReached':
      // Your customer hit their allocation — show YOUR upgrade flow.
      showUpgradeModal(data.payload);
      break;
    case 'error':
      console.error(data.payload.message);
      break;
  }
});

The full message envelope is at PostMessage Protocol.

Always validate the sender

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

Common patterns

Reflect dirty state

JS

JavaScript

let isDirty = false;
editor.on('dirtyChanged', ({ dirty }) => {
  isDirty = dirty;
  document.title = dirty ? '• My Scene' : 'My Scene';
});
 
window.addEventListener('beforeunload', (e) => {
  if (isDirty) {
    e.preventDefault();
    e.returnValue = '';
  }
});

Capture the new ID on first save

When the user saves a brand-new scene, the saved event delivers the new splatId. Persist it on your side and update your host URL, for example /editor?splatId=<splatId>, so refreshes reopen the saved project:

JS

JavaScript

editor.on('saved', async ({ splatId }) => {
  if (splatId) {
    const url = new URL(window.location.href);
    url.searchParams.set('splatId', splatId);
    history.replaceState(null, '', `${url.pathname}${url.search}${url.hash}`);
 
    await fetch('/api/my/projects', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ splatId }),
    });
  }
});

Handle a reached limit (resellers / external users)

When one of your external users hits their per-customer allocation, the editor emits limitReached and shows a neutral, white-label dialog — it never sends your customer to a Moshpit upgrade page, because that billing relationship is yours, not theirs. Use the event to present your own pricing, then raise the customer's cap with PUT /api/v1/external-user:

JS

JavaScript

editor.on('limitReached', ({ dimension, usedBytes, limitBytes }) => {
  // Show your own paywall / upgrade modal — your prices, your branding.
  myUpgradeModal.open({ dimension, usedBytes, limitBytes });
});
 
// After the customer upgrades on your side, raise their Moshpit allocation
// from your backend (never expose the msk_… secret to the browser):
//   PUT /api/v1/external-user
//   X-Moshpit-External-User-Id: <your customer id>
//   { "limitBytes": 10737418240 }   // or { "maxSplats": 50 }

scope tells the two cases apart: 'customer' is the per-customer allocation you set; 'account' means the upload would exceed your Studio account while overage is unconfigured (with overage on, the account accrues overage and never blocks a customer). Either way the customer just sees a neutral "out of storage" message — fixing it is on you (raise the allocation, or configure overage / free account-wide space). Watch your account usage in the Moshpit dashboard.

What's next