Skip to main content
DocsSDKEmbedding Guide
Back to docs

Embedding Guide

H5P player/editor embedding via iframe + postMessage

sdk
embedding

Creatiq Embedding Guide

Overview

Creatiq provides two iframe-based embedding endpoints for rendering H5P content in external applications:

  • Player embed -- renders H5P content for playback and interaction
  • Editor embed -- renders the H5P editor for content creation and editing

Both endpoints live on the Creatiq application server and communicate with the parent window via the postMessage API. The SDK components (CreatiqPlayer and CreatiqEditor) wrap these endpoints, but they can also be used directly via raw iframes.


Player Embedding

URL Format

{CREATIQ_BASE_URL}/embed/{contentId}?token={SDK_TOKEN}

Parameters:

ParameterLocationRequiredDescription
contentIdPathYesH5P content ID to render
tokenQueryYesValid SDK or user JWT token
apiBaseQueryNoOverride API base URL (for reverse proxy setups)

Example:

https://creatiq.app/embed/abc123?token=eyJhbGciOiJ...

How It Works

  1. The embed page loads as a Next.js client component
  2. It validates the token query parameter (shows error if missing)
  3. It calls GET {API_URL}/h5p/play/{contentId} with Authorization: Bearer {token} to fetch the IPlayerModel
  4. When apiBase is provided, all asset URLs (scripts, styles, AJAX paths, content URLs, core URLs) are prefixed with the apiBase value for reverse proxy compatibility
  5. The @lumieducation/h5p-react H5PPlayerUI component renders the content
  6. A postMessage bridge sends events to the parent window

PostMessage Events (Child -> Parent)

creatiq:ready

Sent once when the content has loaded and the player is ready.

{
  type: 'creatiq:ready',
  contentId: string
}

creatiq:xapi

Sent whenever the H5P content emits an xAPI statement (user interaction, completion, scoring).

{
  type: 'creatiq:xapi',
  contentId: string,
  statement: {
    verb: { id: string },
    result?: {
      score?: { scaled?: number, raw?: number, max?: number },
      success?: boolean,
      completion?: boolean,
      duration?: string,   // ISO 8601 duration (e.g., "PT30.5S")
      response?: string
    }
  }
}

The embed page also forwards xAPI events to the Creatiq backend via POST /api/xapi/h5p-event for server-side analytics tracking.

creatiq:resize

Sent when the content's rendered height changes. Emitted on initial render (after 500ms delay) and on every subsequent resize via ResizeObserver.

{
  type: 'creatiq:resize',
  contentId: string,
  height: number          // document.documentElement.scrollHeight in pixels
}

creatiq:error

Sent when content loading or rendering fails.

{
  type: 'creatiq:error',
  contentId: string,
  message: string
}

Raw iframe Example

<iframe
  src="https://creatiq.app/embed/abc123?token=eyJ..."
  title="H5P Content"
  style="width: 100%; border: none; min-height: 200px;"
  allow="fullscreen"
></iframe>

<script>
window.addEventListener('message', (event) => {
  // Validate origin
  if (event.origin !== 'https://creatiq.app') return;

  const data = event.data;
  if (!data || typeof data.type !== 'string' || !data.type.startsWith('creatiq:')) return;

  switch (data.type) {
    case 'creatiq:ready':
      console.log('Content ready:', data.contentId);
      break;
    case 'creatiq:resize':
      document.querySelector('iframe').style.height = data.height + 'px';
      break;
    case 'creatiq:xapi':
      console.log('xAPI:', data.statement);
      break;
    case 'creatiq:error':
      console.error('Error:', data.message);
      break;
  }
});
</script>

Using the SDK Component

The CreatiqPlayer component handles all of the above automatically:

import { CreatiqPlayer } from '@creatiq/ui-sdk/player';

<CreatiqPlayer
  contentId="abc123"
  onXAPIStatement={(stmt) => trackLearning(stmt)}
  onReady={(id) => setLoaded(true)}
  onError={(err) => showError(err)}
  onFullscreen={(active) => toggleFullscreen(active)}
  minHeight={200}
  maxHeight={800}
/>

The component:

  • Builds the iframe URL from config.baseUrl and the authenticated token
  • Validates event.origin against config.baseUrl before processing messages
  • Clamps height between minHeight and maxHeight
  • Shows a spinner overlay until creatiq:ready is received
  • Forwards config.locale and config.theme changes to the iframe via creatiq:locale and creatiq:theme messages

Editor Embedding

URL Format

Edit existing content:

{CREATIQ_BASE_URL}/embed/editor/{contentId}?token={SDK_TOKEN}&language={LANG}

Create new content:

{CREATIQ_BASE_URL}/embed/editor/new?token={SDK_TOKEN}&language={LANG}

Parameters:

ParameterLocationRequiredDescription
contentIdPathYesContent ID, or new for creation
tokenQueryYesValid SDK or user JWT token
languageQueryNoEditor UI language code (default: en)

How It Works

  1. The embed page validates the token parameter
  2. For existing content: calls GET {API_URL}/h5p/edit/{contentId}?language={lang} to fetch IEditorModel with content data
  3. For new content: calls GET {API_URL}/h5p/new?language={lang} to fetch an empty editor model
  4. The @lumieducation/h5p-react H5PEditorUI component renders the editor
  5. The page sets contentLanguage on H5PEditor and H5PIntegration globals for library semantics translation
  6. On save, calls POST /h5p/content (new) or PUT /h5p/content/{contentId} (existing) with the library, params, and metadata
  7. A postMessage bridge sends events to the parent window

PostMessage Events (Child -> Parent)

creatiq:ready

Sent once when the editor has loaded and is ready for user interaction.

{
  type: 'creatiq:ready',
  contentId: string    // For new content, this is "new"
}

creatiq:saved

Sent after content is successfully saved to the backend.

{
  type: 'creatiq:saved',
  contentId: string,   // Server-assigned ID (may differ from "new")
  metadata: {
    title: string,
    mainLibrary: string,
    embedTypes: string[],
    language: string,
    defaultLanguage: string,
    license: string,
    preloadedDependencies: Array<{ machineName: string; majorVersion: number; minorVersion: number }>
  }
}

creatiq:content-changed

Sent when the editor content changes (dirty flag).

{
  type: 'creatiq:content-changed',
  contentId: string
}

creatiq:error

Sent when editor loading or saving fails.

{
  type: 'creatiq:error',
  contentId: string,
  message: string
}

PostMessage Events (Parent -> Child)

The editor embed listens for messages from the parent window:

creatiq:auth

Update the authentication token (for token refresh scenarios).

{
  type: 'creatiq:auth',
  token: string
}

creatiq:locale

Change the editor UI language.

{
  type: 'creatiq:locale',
  locale: string
}

creatiq:theme

Update theme CSS properties.

{
  type: 'creatiq:theme',
  theme: Record<string, string>    // e.g., { colorPrimary: '#6366f1' }
}

Raw iframe Example

<iframe
  id="creatiq-editor"
  src="https://creatiq.app/embed/editor/new?token=eyJ...&language=en"
  title="H5P Content Editor"
  style="width: 100%; height: 700px; border: none;"
  allow="fullscreen"
></iframe>

<script>
window.addEventListener('message', (event) => {
  if (event.origin !== 'https://creatiq.app') return;

  const data = event.data;
  if (!data || typeof data.type !== 'string' || !data.type.startsWith('creatiq:')) return;

  switch (data.type) {
    case 'creatiq:ready':
      console.log('Editor ready');
      break;
    case 'creatiq:saved':
      console.log('Content saved:', data.contentId, data.metadata);
      break;
    case 'creatiq:content-changed':
      // Set dirty flag — prompt before navigation
      window.onbeforeunload = () => 'Unsaved changes';
      break;
    case 'creatiq:error':
      console.error('Editor error:', data.message);
      break;
  }
});
</script>

Using the SDK Component

import { CreatiqEditor } from '@creatiq/ui-sdk/editor';

// Create new content
<CreatiqEditor
  onSave={(result) => {
    console.log('Created content:', result.contentId);
    router.push(`/content/${result.contentId}`);
  }}
  onContentChanged={() => setDirty(true)}
  height={700}
/>

// Edit existing content
<CreatiqEditor
  contentId="abc123"
  language="tr"
  onSave={(result) => {
    console.log('Updated:', result.contentId);
  }}
  onReady={(id) => console.log('Editor ready for', id)}
/>

Authentication

Token Passing

Both embed endpoints require a valid JWT token passed via the token query parameter:

/embed/{contentId}?token=<JWT>
/embed/editor/{contentId}?token=<JWT>

The token is used for:

  • Authorization: Bearer <token> header on all API calls to the Creatiq backend
  • The backend injects the token into H5P AJAX URLs (via addTokenToModel) so that H5P core library requests are also authenticated

Token Exchange Flow

For external applications integrating via the SDK:

  1. Host app backend calls POST /api/sdk/token with the API key to obtain an SDK token scoped to the tenant/user
  2. Host app frontend passes the token to CreatiqProvider via config.token
  3. The SDK injects the token into all API calls and iframe URLs
  4. When the token expires, the SDK calls config.onTokenExpired() to get a fresh token and retries the failed request

Security Notes

  • The token is visible in the iframe URL query string. For production deployments, use short-lived tokens (5-15 minutes) with the onTokenExpired refresh mechanism
  • The embed pages validate the token server-side on every API call
  • The backend's addTokenToModel function injects the auth token into H5P integration URLs, so the SDK should not add it again client-side (double-token causes AJAX failures)

Iframe Configuration

<iframe
  src="..."
  title="Creatiq Content"        <!-- Accessibility: always provide a title -->
  allow="fullscreen"             <!-- Required for H5P fullscreen support -->
  style="
    width: 100%;
    border: none;
    min-height: 200px;           <!-- Player: auto-resized via postMessage -->
  "
></iframe>

Player Sizing

The player embed uses a ResizeObserver on document.body to detect content height changes and sends creatiq:resize messages. The recommended approach:

  1. Set a min-height on the iframe (200px is a reasonable default)
  2. Listen for creatiq:resize messages and update iframe.style.height
  3. Optionally set a max-height to prevent unbounded growth

The SDK's CreatiqPlayer component handles this automatically with the minHeight, maxHeight, and fillContainer props.

Editor Sizing

The editor embed does not send resize messages. Set a fixed height:

<iframe style="width: 100%; height: 700px; border: none;"></iframe>

The SDK's CreatiqEditor defaults to height={600}.

Sandbox Attributes

Do not use the sandbox attribute on the iframe. H5P content requires:

  • Script execution (allow-scripts)
  • Same-origin access (allow-same-origin)
  • Form submission (allow-forms)
  • Popups for some content types (allow-popups)

Using sandbox without all of these will break H5P rendering. If you must use sandbox, include all four:

<iframe sandbox="allow-scripts allow-same-origin allow-forms allow-popups" ...></iframe>

Event Reference

All postMessage Event Types

EventDirectionSourceDescription
creatiq:readyChild -> ParentPlayer, EditorContent/editor has loaded
creatiq:xapiChild -> ParentPlayerxAPI statement from H5P interaction
creatiq:resizeChild -> ParentPlayerContent height changed
creatiq:errorChild -> ParentPlayer, EditorLoading or operation failed
creatiq:fullscreenChild -> ParentPlayerFullscreen state changed
creatiq:savedChild -> ParentEditorContent saved successfully
creatiq:content-changedChild -> ParentEditorEditor content modified (dirty)
creatiq:authParent -> ChildSDKToken refresh
creatiq:localeParent -> ChildSDKLocale change
creatiq:themeParent -> ChildSDKTheme update

Type Guards

The SDK provides type guards for filtering postMessage events:

import { isCreatiqMessage, isCreatiqParentMessage } from '@creatiq/ui-sdk/player';

window.addEventListener('message', (event) => {
  if (isCreatiqMessage(event.data)) {
    // event.data is narrowed to ChildMessage
    const msg = event.data;
    switch (msg.type) {
      case 'creatiq:xapi':
        handleXAPI(msg.statement);
        break;
    }
  }
});

Both guards check that the data is a non-null object with a type string property starting with creatiq:.

Origin Validation

The SDK components validate event.origin against the configured config.baseUrl before processing any message. When using raw iframes, always validate the origin:

window.addEventListener('message', (event) => {
  const expectedOrigin = new URL('https://creatiq.app').origin;
  if (event.origin !== expectedOrigin) return;
  // Safe to process
});

The embed pages send messages to window.parent with '*' as the target origin (since they do not know the host app's origin). This is safe because the messages contain no secrets -- they carry only content IDs, xAPI statements, and UI state.


Reverse Proxy Support

When the Creatiq API is served through a reverse proxy with a path prefix, use the apiBase query parameter:

/embed/{contentId}?token=...&apiBase=https://myapp.com/creatiq-api

The embed page will prefix all H5P asset URLs (scripts, styles, AJAX paths, content URLs, core URLs) with the apiBase value. This ensures H5P core and library resources resolve through the proxy.

Back to docsdocs/product/sdk/embedding.md