Skip to main content
The browser side of Canvas Embed SDK is a small postMessage protocol between your page and the Moda iframe.
The beta protocol version is 2026-05-27. Always send and validate the channel, version, and sessionId fields.
There are two version numbers, and they are not the same. They live in different layers and are set in different places:They version independently and will diverge further over time. The iframe silently drops any message whose version doesn’t match the protocol version, so a transposed value fails with no error — send 2026-05-27 in the envelope, and 2026-05-01 in the API header. See Session API for the header.

Message envelope

Every command and event uses the same envelope:
Use requestId to correlate command responses.

Minimal helper

moda-embed-client.ts
The iframe you pass in needs the right capability attributes — clipboard paste and the file picker for image upload depend on them:
Export downloads happen in your page (you call a.click() on the returned dataUrl), so the iframe needs no downloads permission. See Security and Production for the full rationale and CSP guidance.

Handshake and lifecycle

There are two distinct milestones, signalled by two different events:
  1. Session ready — the iframe posts ready once, as soon as it has loaded its session. Payload is minimal: { canvasId, mode, externalUser? }. After this you can send session-scoped commands (save, chat.send, setReadonly).
  2. Canvas interactive — the canvas finishes loading and rendering separately. This is signalled by canvas:changed with { status: 'loaded', page, pageCount, category, pages } (and an initial page:changed). Gate page/export/edit commands (focusPage, export, insertText, insertImage) on this, not on ready.
ready does not fire a second time with page info — page state arrives via canvas:changed/page:changed. (You can get a ready carrying page state, but only as a reply to host.hello — see below.)

Use host.hello to win the load race

The iframe’s first ready can fire before your page has attached its message listener (a classic iframe-load race). host.hello is the robust fix: the iframe replies with ready every time it receives one, and that reply includes the current page state if the canvas is already interactive. Poll it until you get a ready, then stop:

Commands

Save

Events:
  • canvas:saving
  • canvas:saved
  • canvas:save_error

Export

PDF exports emit a selectable text layer by default. Pass flatten: true to rasterize every page instead — useful when a canvas uses a custom font whose glyphs must be reproduced exactly, at the cost of selectable text and a larger file:
export:completed returns the file inline as a base64 dataUrl — the host triggers the download, the iframe never does:
The entire export is base64-encoded and passed inline through postMessage — there is no size cap, chunking, or hosted-URL fallback, and the SDK times out after ~120s. A single PNG/JPEG is fine, but a multi-page PDF or PPTX (or a multi-page raster export, which comes back as a data:application/zip URL) can run to many megabytes in memory. For large or multi-page documents, prefer the Public API export — it returns a durable URL and supports background polling.

Focus a page

Event:
  • page:changed with { page, pageCount, category, pages }

Toggle readonly

This is useful when your app needs to temporarily block editing while another workflow is running. It cannot grant editing to a non-edit session.

Insert text

Insert image

Use File or Blob when the user picks or pastes a local image. The iframe uploads it using its scoped embed browser credential. Embed uploads currently accept PNG, JPEG, GIF, and WebP files up to 25 MB. SVG uploads are not accepted in embedded sessions.
Use url when your app already has an HTTPS image URL that Moda can render.

Chat modes

For ui.chat: "inside", the iframe renders Moda’s hosted chat panel. Your app does not need to send chat commands, but it can still listen for chat:* events for analytics, logging, or mirrored UI. For ui.chat: "external", your app owns the chat UI and sends prompts to the Moda agent.

chat:message — the one that matters

Each chat:message is a complete, self-contained message appended to the conversation — not an incremental delta. The text lives in text, and the author in sender. Do not accumulate or diff these; render each as its own bubble keyed by id.
Incremental, in-flight streaming (thinking, tool steps, progress) is surfaced through chat:progress and chat:tool_call, never as chat:message deltas.

All chat events

Refresh session

The iframe proactively emits session:refresh_requested ahead of expiry (by default 60 seconds before the browser token lapses), so you can refresh in response instead of running your own timer:
If a refresh doesn’t land in time, the iframe emits session:expired (or session:revoked) so you can surface a re-authentication prompt.

Events

Keyboard and paste behavior

When the iframe has focus, Moda handles normal editor shortcuts:
  • Undo/redo
  • Copy, cut, paste
  • Pasted image files and screenshots
  • Delete, duplicate, group, ungroup, arrange
  • Arrow nudging
  • Tool shortcuts like V, T, R, O, L, and P
When your parent app has focus, keyboard events belong to your app. If you want parent-level buttons or shortcuts, send explicit commands such as save, export, or chat.send.