Skip to main content
Embed sessions are short-lived sessions scoped to one canvas. Your backend creates them with a Moda API key, then passes the returned embed_url to your frontend. The URL contains a short-lived boot token; the iframe exchanges it for a narrower browser credential before calling canvas, asset, export, or chat endpoints.

Create an embed session

Moda-Version: 2026-05-01 is the REST API version. It is not the same as the browser protocol version: 2026-05-27 you send in postMessage envelopes — see the Browser SDK version note. They version independently.
Request
Response
Edit-mode session creation also verifies that the API key owner can edit the canvas. If not, the API returns 403.

Request fields

Modes

UI options

chat controls where the Moda agent chat appears:
  • "hidden" disables agent chat for the embed session.
  • "inside" renders a Moda-hosted chat panel inside the iframe.
  • "external" enables the agent runtime but leaves the chat UI to your application through postMessage commands and events.

Origin validation

allowed_origins must contain origins only: scheme, host, and optional port. Do not include paths.
If the iframe receives a message from an origin that is not in this list, it ignores it.

Refresh a session

Refresh rotates the session boot token for an existing session and returns a fresh browser token for the iframe. Call this from your backend before expiry, then pass the new values to the iframe with session.refresh.
Refresh only works on a live session. Once a session has expired or been revoked, refresh returns 404 (record gone) or 409 (expired/revoked record still present) and can never succeed again — do not retry it on a timer. Keep retrying the same session_id and the API starts answering 410 Gone (embed_session_gone), which is terminal: there is no Retry-After and no amount of waiting makes that id work. Recover by creating a new session and swapping the iframe to the new embed_url; see Recover from full expiry.
Response
Then tell the iframe:
The iframe acknowledges a successful refresh by posting session:refreshed with { expiresAt }. The iframe also proactively warns you before expiry by posting session:refresh_requested (by default 60 seconds before expiry), and posts session:expired if the token lapses — refresh in response to the warning (see Refresh long-running sessions).

Revoke a session

Call revoke when the user closes the embedded editor, signs out, or loses access in your product.

Error codes

Every public API error — including session create/refresh/revoke and the internal iframe endpoints — returns the same envelope:
Session failures map to these HTTP statuses and code categories:
Expired, revoked, and invalid-token failures surface as 401 / authentication everywhere except the refresh endpoint — on the boot-token exchange (GET /v1/embed/sessions/{session_id}?token=…) as well as on the browser token; today they are distinguished only by the human-readable message ("Embed session expired", "Embed session revoked", and an invalid-token string that differs by lane — "Invalid embed browser token" on the browser credential, "Invalid embed session token" on the boot exchange). Branch on status for recovery (re-mint on 401, surface a permission error on 403), and treat message as non-stable display text. Finer-grained machine codes are a known beta limitation.The refresh endpoint is the exception, and the distinction there is load-bearing: 409 means the session is dead but its record survives, 410 means stop asking about this id entirely, and 429 is the only one of the three worth retrying. A generic “retry on 4xx” policy will loop forever on 404 and 410.Read error.retryable first. It is the server’s explicit contract for that code — false means retrying the same request cannot succeed, true means the condition is transient — and it is correct for codes this table has never heard of. Do not derive retryability from the status when the field is present.It is not on every response, and whether it appears is a property of the code, not of how the error is raised: the field is sent only for codes whose registry entry declares it, and omitted entirely otherwise (null keys are stripped from the envelope, so it is absent rather than null). On this lane embed_session_gone and canvas_crdt_state_corrupt send false and rate_limited sends true, while not_found, conflict, authentication, permission, upstream_error and collab_active send nothing — including the typed collab_active, which is why “typed” is not the test. Expect the fallback below to be the usual path here rather than the exception. When retryable is absent the code’s retryability is context-dependent; branch on status and code then, remembering the mapping is not 1:1 and the table above is the current set rather than a closed one. 409 alone carries conflict, collab_active and canvas_crdt_state_corrupt, and they disagree about retrying. For an unrecognised 4xx code, not retrying is the safe default; do not extend that to 5xx, where transient is the norm and retry_after_ms may carry a hint.
When one of these failures happens inside the loaded iframe (for example, the browser token expires mid-session), the iframe forwards it to your page as a load:error event carrying the same code and status.

Internal iframe endpoints

The iframe exchanges the boot URL token for a short-lived embed browser credential, then uses that credential for internal browser-hit endpoints:
  • GET /v1/embed/sessions/{session_id}/canvas
  • PATCH /v1/embed/sessions/{session_id}/canvas
  • POST /v1/embed/sessions/{session_id}/images
  • GET /v1/embed/sessions/{session_id}/images/{file_id}
These calls use Authorization: Bearer <embed_browser_token>. You normally do not call these directly. Use the iframe and browser SDK commands instead.