> ## Documentation Index
> Fetch the complete documentation index at: https://docs.moda.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Quickstart

> Create an embed session, load the iframe, and send your first command.

This guide creates an edit-mode embed with the secondary toolbar and Moda's hosted chat panel inside the iframe.

## Prerequisites

* A Moda API key enabled for Canvas Embed SDK
* A canvas ID, for example `cvs_01HT9WK8N3M2J4A5Z6P7Q8R9TV`
* The exact origin of the page that will host the iframe, for example `https://app.example.com`

<Warning>
  Never create embed sessions directly from browser code. Your Moda API key must stay on your backend.
</Warning>

## 1. Create a session on your backend

```ts title="server/create-moda-embed-session.ts" theme={null}
const MODA_API_URL = 'https://api.moda.app/v1';

export async function createModaEmbedSession({
  canvasId,
  externalUser,
}: {
  canvasId: string;
  externalUser: { id: string; email?: string; name?: string };
}) {
  const response = await fetch(`${MODA_API_URL}/embed/sessions`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.MODA_API_KEY}`,
      'Content-Type': 'application/json',
      'Moda-Version': '2026-05-01',
    },
    body: JSON.stringify({
      canvas_id: canvasId,
      mode: 'edit',
      external_user: externalUser,
      allowed_origins: ['https://app.example.com'],
      ui: {
        chrome: 'minimal',
        toolbar: 'secondary',
        chat: 'inside',
        speaker_notes: 'hidden',
        theme: 'light',
      },
      expires_in_seconds: 3600,
    }),
  });

  if (!response.ok) {
    const body = await response.json().catch(() => null);
    throw new Error(body?.error?.message || body?.detail || `Moda embed failed: ${response.status}`);
  }

  return response.json() as Promise<{
    session_id: string;
    embed_url: string;
    expires_at: string;
  }>;
}
```

## 2. Return only the embed URL to your frontend

```ts title="app/api/moda-embed/route.ts" theme={null}
export async function POST(request: Request) {
  const { canvasId } = await request.json();

  const session = await createModaEmbedSession({
    canvasId,
    externalUser: {
      id: 'user_123',
      email: 'alice@example.com',
      name: 'Alice',
    },
  });

  return Response.json(session);
}
```

## 3. Load the iframe

```tsx title="ModaCanvasEmbed.tsx" theme={null}
import { useEffect, useRef, useState } from 'react';

const CHANNEL = 'moda.embed';
const VERSION = '2026-05-27';

type ModaEmbedMessage = {
  channel: typeof CHANNEL;
  version: typeof VERSION;
  sessionId: string;
  requestId?: string;
  type: string;
  payload?: unknown;
};

export function ModaCanvasEmbed({ canvasId }: { canvasId: string }) {
  const iframeRef = useRef<HTMLIFrameElement>(null);
  const [session, setSession] = useState<{ session_id: string; embed_url: string } | null>(null);
  const [ready, setReady] = useState(false);

  useEffect(() => {
    fetch('/api/moda-embed', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ canvasId }),
    })
      .then((response) => response.json())
      .then(setSession);
  }, [canvasId]);

  useEffect(() => {
    if (!session) return;
    const targetOrigin = new URL(session.embed_url).origin;

    function onMessage(event: MessageEvent<ModaEmbedMessage>) {
      if (event.origin !== targetOrigin) return;
      if (event.source !== iframeRef.current?.contentWindow) return;
      const message = event.data;
      if (message?.channel !== CHANNEL || message.version !== VERSION) return;
      if (message.sessionId !== session.session_id) return;

      if (message.type === 'ready') {
        setReady(true);
      }
    }

    window.addEventListener('message', onMessage);
    return () => window.removeEventListener('message', onMessage);
  }, [session]);

  function post(type: string, payload?: unknown) {
    if (!session || !iframeRef.current) return;
    iframeRef.current.contentWindow?.postMessage(
      {
        channel: CHANNEL,
        version: VERSION,
        sessionId: session.session_id,
        requestId: crypto.randomUUID(),
        type,
        payload,
      },
      new URL(session.embed_url).origin
    );
  }

  if (!session) return <div>Loading...</div>;

  return (
    <div style={{ display: 'grid', gridTemplateRows: '40px 1fr', height: '100vh' }}>
      <div>
        <button disabled={!ready} onClick={() => post('save')}>Save</button>
        <button disabled={!ready} onClick={() => post('export', { format: 'png', page: 1 })}>Export PNG</button>
      </div>
      <iframe ref={iframeRef} src={session.embed_url} title="Moda canvas" style={{ width: '100%', height: '100%', border: 0 }} />
    </div>
  );
}
```

## 4. Listen for results

At minimum, handle these events:

| Event                                                              | Use it for                                               |
| ------------------------------------------------------------------ | -------------------------------------------------------- |
| `ready`                                                            | Enable parent controls after the iframe accepts commands |
| `size:changed`                                                     | Resize the iframe container if your layout is dynamic    |
| `canvas:saving` / `canvas:saved` / `canvas:save_error`             | Show save state                                          |
| `export:completed` / `export:failed`                               | Download or display exports                              |
| `chat:message` / `chat:progress` / `chat:completed` / `chat:error` | Observe hosted chat or drive an external chat UI         |
| `load:error`                                                       | Show a recoverable error state                           |

## Common first configuration

```json title="Create session body" theme={null}
{
  "canvas_id": "cvs_01HT9WK8N3M2J4A5Z6P7Q8R9TV",
  "mode": "edit",
  "external_user": {
    "id": "customer-user-123",
    "email": "alice@example.com",
    "name": "Alice"
  },
  "allowed_origins": ["https://app.example.com"],
  "ui": {
    "chrome": "minimal",
    "toolbar": "secondary",
    "chat": "inside",
    "speaker_notes": "hidden",
    "theme": "light"
  },
  "expires_in_seconds": 3600
}
```

Use `chat: "external"` when you want your application to own the chat UI and send `chat.send`, `chat.stop`, and `chat.newThread` commands itself.

For the full session contract, see [Session API](/canvas-embed/session-api).
