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:
| Parameter | Location | Required | Description |
|---|---|---|---|
contentId | Path | Yes | H5P content ID to render |
token | Query | Yes | Valid SDK or user JWT token |
apiBase | Query | No | Override API base URL (for reverse proxy setups) |
Example:
https://creatiq.app/embed/abc123?token=eyJhbGciOiJ...
How It Works
- The embed page loads as a Next.js client component
- It validates the
tokenquery parameter (shows error if missing) - It calls
GET {API_URL}/h5p/play/{contentId}withAuthorization: Bearer {token}to fetch theIPlayerModel - When
apiBaseis provided, all asset URLs (scripts, styles, AJAX paths, content URLs, core URLs) are prefixed with theapiBasevalue for reverse proxy compatibility - The
@lumieducation/h5p-reactH5PPlayerUIcomponent renders the content - A
postMessagebridge 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.baseUrland the authenticated token - Validates
event.originagainstconfig.baseUrlbefore processing messages - Clamps height between
minHeightandmaxHeight - Shows a spinner overlay until
creatiq:readyis received - Forwards
config.localeandconfig.themechanges to the iframe viacreatiq:localeandcreatiq:thememessages
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:
| Parameter | Location | Required | Description |
|---|---|---|---|
contentId | Path | Yes | Content ID, or new for creation |
token | Query | Yes | Valid SDK or user JWT token |
language | Query | No | Editor UI language code (default: en) |
How It Works
- The embed page validates the
tokenparameter - For existing content: calls
GET {API_URL}/h5p/edit/{contentId}?language={lang}to fetchIEditorModelwith content data - For new content: calls
GET {API_URL}/h5p/new?language={lang}to fetch an empty editor model - The
@lumieducation/h5p-reactH5PEditorUIcomponent renders the editor - The page sets
contentLanguageonH5PEditorandH5PIntegrationglobals for library semantics translation - On save, calls
POST /h5p/content(new) orPUT /h5p/content/{contentId}(existing) with the library, params, and metadata - A
postMessagebridge 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:
- Host app backend calls
POST /api/sdk/tokenwith the API key to obtain an SDK token scoped to the tenant/user - Host app frontend passes the token to
CreatiqProviderviaconfig.token - The SDK injects the token into all API calls and iframe URLs
- 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
onTokenExpiredrefresh mechanism - The embed pages validate the token server-side on every API call
- The backend's
addTokenToModelfunction 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
Recommended Attributes
<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:
- Set a
min-heighton the iframe (200px is a reasonable default) - Listen for
creatiq:resizemessages and updateiframe.style.height - Optionally set a
max-heightto 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
| Event | Direction | Source | Description |
|---|---|---|---|
creatiq:ready | Child -> Parent | Player, Editor | Content/editor has loaded |
creatiq:xapi | Child -> Parent | Player | xAPI statement from H5P interaction |
creatiq:resize | Child -> Parent | Player | Content height changed |
creatiq:error | Child -> Parent | Player, Editor | Loading or operation failed |
creatiq:fullscreen | Child -> Parent | Player | Fullscreen state changed |
creatiq:saved | Child -> Parent | Editor | Content saved successfully |
creatiq:content-changed | Child -> Parent | Editor | Editor content modified (dirty) |
creatiq:auth | Parent -> Child | SDK | Token refresh |
creatiq:locale | Parent -> Child | SDK | Locale change |
creatiq:theme | Parent -> Child | SDK | Theme 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.