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
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
| Event | Payload | Fires |
|---|---|---|
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':
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
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:
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:
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
- Editor Commands — what you can send back.
- External Users — provision and re-allocate customer quotas.
- Viewer Events — the read-only counterpart.