Skip to main content
Custom content lets you create fully interactive applications that run inside entity pages. Your app runs in a sandboxed iframe and can read/write entity data via the REST API.

How it works

Seal injects window.SEAL_CONTEXT into your HTML with credentials to access the API.

Context

Your app receives this context automatically:
interface SealContext {
  entityId: string;      // Current entity ID
  apiToken: string;      // Bearer token for API calls
  apiUrl: string;        // API base URL
  version: string | null; // Snapshot version (null = draft)
}

API examples

const { entityId, apiToken, apiUrl } = window.SEAL_CONTEXT;

const entity = await fetch(`${apiUrl}/entities/${entityId}`, {
  headers: { Authorization: `Bearer ${apiToken}` }
}).then(r => r.json());

Development

1

Start local dev server

Run your app on port 5347:
bun dev
2

Enter development mode

Open a custom content entity in Seal and click Start Development Mode.
3

Handle context

In dev mode, context is sent via postMessage:
let ctx = window.SEAL_CONTEXT;

window.addEventListener('message', (e) => {
  if (e.data?.type === 'SEAL_CONTEXT') ctx = e.data.payload;
});

if (!ctx) window.parent.postMessage({ type: 'REQUEST_CONTEXT' }, '*');
Your code works in both dev mode (postMessage) and production (injected context) with the pattern above.

Deploy

1

Build your app

bun build
2

Create zip

zip -r build.zip dist/
The zip must contain index.html at the root.
3

Deploy

curl -X POST "$SEAL_URL/api/custom-content/$ENTITY_ID/deploy" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/zip" \
  --data-binary @build.zip

Complete example

A minimal app that fetches and displays the current entity:
index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Custom Content</title>
  </head>
  <body>
    <div id="app">Loading...</div>
    <script>
      let ctx = window.SEAL_CONTEXT;

      // Dev mode: receive context via postMessage
      window.addEventListener('message', (e) => {
        if (e.data?.type === 'SEAL_CONTEXT') {
          ctx = e.data.payload;
          init();
        }
      });

      if (ctx) {
        init();
      } else {
        window.parent.postMessage({ type: 'REQUEST_CONTEXT' }, '*');
      }

      function init() {
        fetch(`${ctx.apiUrl}/entities/${ctx.entityId}`, {
          headers: { Authorization: `Bearer ${ctx.apiToken}` }
        })
          .then(r => r.json())
          .then(entity => {
            document.getElementById('app').innerHTML =
              `<pre>${JSON.stringify(entity, null, 2)}</pre>`;
          })
          .catch(e => {
            document.getElementById('app').innerHTML =
              `<p style="color:red">${e}</p>`;
          });
      }
    </script>
  </body>
</html>

API reference

Full REST API documentation
The SDK uses postMessage instead of REST API. Both work, but REST API is recommended for new apps.