Embedding Integration Guide
Overview
Creatiq provides two approaches for embedding H5P content into your application:
-
Iframe Embed -- Load the Creatiq embed pages directly in an
<iframe>. Works with any web framework or vanilla HTML. Requires constructing URLs and listening forpostMessageevents manually. -
SDK Embed -- Install
@creatiq/ui-sdkand use React components (<CreatiqPlayer>,<CreatiqEditor>,<CreatiqWorkspace>) that handle iframe management, authentication, theming, and event bridging automatically.
Both approaches require a valid authentication token obtained from your backend.
Iframe Player
The player embed renders H5P content in a minimal chrome-free page suitable for iframe embedding.
URL Format
{CREATIQ_BASE_URL}/embed/{contentId}?token={token}
| Parameter | Required | Description |
|---|---|---|
contentId | Yes | The H5P content ID to display |
token | Yes | Bearer authentication token |
apiBase | No | Override API base URL (useful for reverse proxy setups) |
Example
<iframe
src="https://creatiq.app/embed/abc123?token=eyJ..."
style="width: 100%; border: none; min-height: 400px;"
allow="fullscreen"
title="Interactive Content"
></iframe>
PostMessage Events (Child to Parent)
The embed page communicates with the parent window via postMessage. All messages are prefixed with creatiq: to avoid collisions.
| Event Type | Payload | Description |
|---|---|---|
creatiq:ready | { contentId } | Content has loaded and is ready |
creatiq:xapi | { contentId, statement } | An xAPI statement was generated |
creatiq:resize | { contentId, height } | Content height changed (for auto-resize) |
creatiq:error | { contentId, message } | An error occurred loading or playing content |
creatiq:fullscreen | { contentId, active } | Fullscreen state changed |
Auto-Resize
The embed page uses a ResizeObserver on the document body and sends creatiq:resize messages when the content height changes. Implement auto-resize in the parent:
window.addEventListener('message', (event) => {
if (event.data?.type === 'creatiq:resize') {
const iframe = document.getElementById('creatiq-iframe');
if (iframe) {
iframe.style.height = event.data.height + 'px';
}
}
});
PostMessage Commands (Parent to Child)
You can send messages to the embed iframe:
| Command Type | Payload | Description |
|---|---|---|
creatiq:auth | { token } | Refresh the authentication token |
creatiq:locale | { locale } | Change the UI locale |
creatiq:theme | { theme } | Apply theme overrides (key-value CSS properties) |
const iframe = document.getElementById('creatiq-iframe');
iframe.contentWindow.postMessage(
{ type: 'creatiq:locale', locale: 'tr' },
'https://creatiq.app'
);
Iframe Editor
The editor embed renders the H5P editor for creating or editing content.
URL Format
{CREATIQ_BASE_URL}/embed/editor/{contentId}?token={token}&language={lang}
For new content, use new as the contentId:
{CREATIQ_BASE_URL}/embed/editor/new?token={token}&language=en
| Parameter | Required | Description |
|---|---|---|
contentId | Yes | Content ID to edit, or new for creation |
token | Yes | Bearer authentication token |
language | No | Editor UI language (default: en) |
Example
<iframe
src="https://creatiq.app/embed/editor/new?token=eyJ...&language=en"
style="width: 100%; height: 700px; border: none;"
allow="fullscreen"
title="Content Editor"
></iframe>
Editor PostMessage Events
In addition to creatiq:ready and creatiq:error, the editor emits:
| Event Type | Payload | Description |
|---|---|---|
creatiq:saved | { contentId, metadata } | Content was saved (contentId may differ from URL for new content) |
creatiq:content-changed | { contentId } | Content has unsaved changes (dirty flag) |
Save Flow
The editor handles saving internally. When the user clicks Save in the H5P editor:
- Content is sent to
POST /h5p/content(new) orPUT /h5p/content/{contentId}(edit) - A
creatiq:savedmessage is posted to the parent with the resultingcontentIdandmetadata
Listen for the save event to update your application state:
window.addEventListener('message', (event) => {
if (event.data?.type === 'creatiq:saved') {
console.log('Saved content:', event.data.contentId);
console.log('Metadata:', event.data.metadata);
// Update your database with the new content ID
}
});
SDK Approach
The @creatiq/ui-sdk package provides React components that wrap the iframe embeds with automatic authentication, theming, resizing, and event handling.
Installation
npm install @creatiq/ui-sdk
# or
pnpm add @creatiq/ui-sdk
Peer dependencies (must be installed separately):
npm install react react-dom
Import the Stylesheet
import '@creatiq/ui-sdk/styles.css';
Setup Provider
Wrap your application (or the relevant subtree) with <CreatiqProvider>:
import { CreatiqProvider } from '@creatiq/ui-sdk';
function App() {
return (
<CreatiqProvider
config={{
baseUrl: 'https://creatiq.app',
token: 'your-sdk-token',
locale: 'en',
theme: {
colorPrimary: '#6366f1',
borderRadius: '0.5rem',
},
onTokenExpired: async () => {
// Fetch a fresh token from your backend
const res = await fetch('/api/creatiq/refresh-token');
const data = await res.json();
return data.token;
},
}}
>
<YourApp />
</CreatiqProvider>
);
}
CreatiqConfig Reference
| Property | Type | Required | Description |
|---|---|---|---|
baseUrl | string | Yes | Creatiq API base URL |
token | string | Yes | SDK authentication token |
locale | 'en' | 'tr' | 'fr' | 'es' | 'ar' | No | UI locale |
theme | Partial<CreatiqTheme> | No | Theme overrides |
ai | CreatiqAIConfig | No | AI feature configuration |
onTokenExpired | () => Promise<string> | No | Token refresh callback |
onLogout | () => void | No | Logout callback |
onLocaleChange | (locale: string) => void | No | Locale change callback |
tenantId | string | No | Tenant identifier |
CreatiqTheme Properties
| Property | CSS Variable | Description |
|---|---|---|
colorPrimary | --crq-color-primary | Primary brand color |
colorPrimaryForeground | --crq-color-primary-foreground | Text on primary color |
colorBackground | --crq-color-background | Page background |
colorSurface | --crq-color-surface | Card/surface background |
colorText | --crq-color-text | Primary text color |
colorMuted | --crq-color-muted | Muted/secondary text |
colorBorder | --crq-color-border | Border color |
colorDestructive | --crq-color-destructive | Error/destructive color |
borderRadius | --crq-border-radius | Global border radius |
fontFamily | --crq-font-family | Font family |
Player Component
import { CreatiqPlayer } from '@creatiq/ui-sdk';
function ContentViewer({ contentId }: { contentId: string }) {
return (
<CreatiqPlayer
contentId={contentId}
minHeight={300}
onReady={(id) => console.log('Content ready:', id)}
onXAPIStatement={(statement) => {
console.log('xAPI:', statement);
}}
onError={(error) => console.error('Player error:', error)}
/>
);
}
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
contentId | string | -- | H5P content ID (required) |
onXAPIStatement | (statement: unknown) => void | -- | xAPI event callback |
onReady | (contentId: string) => void | -- | Content loaded callback |
onError | (error: string) => void | -- | Error callback |
onFullscreen | (active: boolean) => void | -- | Fullscreen toggle callback |
className | string | -- | Wrapper div class |
minHeight | number | 200 | Minimum height in pixels |
maxHeight | number | -- | Maximum height in pixels |
fillContainer | boolean | false | Use 100% height instead of auto-resize |
Editor Component
import { CreatiqEditor } from '@creatiq/ui-sdk';
// Edit existing content
function ContentEditor({ contentId }: { contentId: string }) {
return (
<CreatiqEditor
contentId={contentId}
height={700}
onSave={(result) => {
console.log('Saved:', result.contentId, result.metadata);
}}
onContentChanged={() => {
console.log('Content has unsaved changes');
}}
/>
);
}
// Create new content
function ContentCreator() {
return (
<CreatiqEditor
height={700}
onSave={(result) => {
// result.contentId is the newly assigned ID
console.log('Created:', result.contentId);
}}
/>
);
}
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
contentId | string | -- | Content ID to edit (omit for new content) |
language | string | config locale | Editor UI language |
onSave | (result: { contentId, metadata }) => void | -- | Save callback |
onContentChanged | () => void | -- | Dirty flag callback |
onError | (error: string) => void | -- | Error callback |
onReady | (contentId: string) => void | -- | Editor loaded callback |
className | string | -- | Wrapper div class |
height | number | 600 | Editor height in pixels |
Workspace Component
The full-featured workspace combines content browsing, creation, collections, and settings:
import { CreatiqWorkspace } from '@creatiq/ui-sdk';
function MyWorkspace() {
return (
<CreatiqWorkspace
defaultView="collections"
onContentSelect={(content) => {
console.log('Selected:', content.id);
}}
/>
);
}
Content Picker
For selecting content (e.g., attaching to a session):
import { ContentPickerSheet } from '@creatiq/ui-sdk';
function AttachContent() {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>Attach Content</button>
<ContentPickerSheet
open={open}
onOpenChange={setOpen}
onSelect={(items) => {
console.log('Selected:', items);
setOpen(false);
}}
/>
</>
);
}
Context Hooks
Access SDK state from any descendant component:
import {
useCreatiq, // Full context (config, user, features, getToken, refreshToken, apiUrl)
useCreatiqConfig, // SDK configuration only
useCreatiqUser, // Authenticated user (null if loading)
useCreatiqFeatures // Plan features and limits (null if loading)
} from '@creatiq/ui-sdk';
function UserInfo() {
const user = useCreatiqUser();
const features = useCreatiqFeatures();
if (!user) return <p>Loading...</p>;
return (
<div>
<p>{user.name} ({user.plan})</p>
<p>Remaining generations: {features?.limits.remainingGenerations}</p>
</div>
);
}
Authentication
Both iframe and SDK approaches require a token. Tokens are obtained from the Creatiq backend.
Token Exchange Flow
- Your backend authenticates the user through your own auth system
- Your backend calls the Creatiq API to obtain an SDK token:
POST {CREATIQ_BASE_URL}/api/sdk/token
Content-Type: application/json
{
"apiKey": "your-creatiq-api-key",
"userId": "your-user-id",
"email": "user@example.com",
"name": "User Name",
"role": "teacher"
}
- The token is returned to your frontend and used in the iframe URL or SDK config
Token Refresh
Tokens expire after a configured period. Handle expiration:
Iframe approach: Listen for 401 errors and post a new token:
// When you detect the token has expired
iframe.contentWindow.postMessage(
{ type: 'creatiq:auth', token: 'new-token-here' },
'https://creatiq.app'
);
SDK approach: Provide the onTokenExpired callback in CreatiqConfig:
<CreatiqProvider
config={{
baseUrl: 'https://creatiq.app',
token: initialToken,
onTokenExpired: async () => {
const response = await fetch('/api/creatiq/refresh-token');
const { token } = await response.json();
return token;
},
}}
>
The SDK automatically calls onTokenExpired when API requests fail with 401 and retries the request with the new token.
Examples
Vanilla JavaScript -- Player
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Creatiq Embed</title>
<style>
#player-container { width: 100%; max-width: 800px; margin: 0 auto; }
#creatiq-iframe { width: 100%; border: none; min-height: 400px; border-radius: 12px; }
</style>
</head>
<body>
<div id="player-container">
<iframe
id="creatiq-iframe"
src="https://creatiq.app/embed/CONTENT_ID?token=YOUR_TOKEN"
allow="fullscreen"
title="Interactive Content"
></iframe>
</div>
<script>
const iframe = document.getElementById('creatiq-iframe');
window.addEventListener('message', (event) => {
// Validate origin
if (event.origin !== 'https://creatiq.app') return;
const { type, contentId, height, statement, message } = event.data || {};
switch (type) {
case 'creatiq:ready':
console.log('Content ready:', contentId);
break;
case 'creatiq:resize':
iframe.style.height = height + 'px';
break;
case 'creatiq:xapi':
console.log('xAPI statement:', statement);
// Forward to your LRS or analytics
break;
case 'creatiq:error':
console.error('Embed error:', message);
break;
}
});
</script>
</body>
</html>
React -- SDK Player and Editor
import { useState } from 'react';
import { CreatiqProvider, CreatiqPlayer, CreatiqEditor } from '@creatiq/ui-sdk';
import '@creatiq/ui-sdk/styles.css';
function App() {
const [token] = useState('your-sdk-token');
return (
<CreatiqProvider
config={{
baseUrl: 'https://creatiq.app',
token,
locale: 'en',
theme: {
colorPrimary: '#2563eb',
borderRadius: '0.75rem',
fontFamily: 'Inter, sans-serif',
},
onTokenExpired: async () => {
const res = await fetch('/api/creatiq/token');
const data = await res.json();
return data.token;
},
}}
>
<div style={{ maxWidth: 960, margin: '0 auto', padding: 24 }}>
<h1>My Learning Platform</h1>
{/* Play existing content */}
<section>
<h2>Interactive Quiz</h2>
<CreatiqPlayer
contentId="quiz-123"
minHeight={400}
onXAPIStatement={(stmt) => {
fetch('/api/analytics/xapi', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(stmt),
});
}}
onReady={() => console.log('Quiz loaded')}
/>
</section>
{/* Edit or create content */}
<section>
<h2>Content Editor</h2>
<CreatiqEditor
height={700}
onSave={(result) => {
alert(`Content saved: ${result.contentId}`);
}}
/>
</section>
</div>
</CreatiqProvider>
);
}
export default App;
React -- Content Picker Integration
import { useState } from 'react';
import {
CreatiqProvider,
ContentPickerSheet,
type SelectedContentItem,
} from '@creatiq/ui-sdk';
import '@creatiq/ui-sdk/styles.css';
function SessionContentAttacher({ sessionId }: { sessionId: string }) {
const [pickerOpen, setPickerOpen] = useState(false);
const [attached, setAttached] = useState<SelectedContentItem[]>([]);
const handleSelect = async (items: SelectedContentItem[]) => {
setAttached((prev) => [...prev, ...items]);
setPickerOpen(false);
// Save to your backend
await fetch(`/api/sessions/${sessionId}/contents`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ contentIds: items.map((i) => i.id) }),
});
};
return (
<div>
<h3>Session Content</h3>
<ul>
{attached.map((item) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
<button onClick={() => setPickerOpen(true)}>
+ Add Content
</button>
<ContentPickerSheet
open={pickerOpen}
onOpenChange={setPickerOpen}
onSelect={handleSelect}
/>
</div>
);
}
Package Exports
The @creatiq/ui-sdk package provides tree-shakeable sub-path exports:
| Import Path | Contents |
|---|---|
@creatiq/ui-sdk | Provider, hooks, player, editor, picker, workspace, types |
@creatiq/ui-sdk/player | CreatiqPlayer, message protocol utilities |
@creatiq/ui-sdk/editor | CreatiqEditor |
@creatiq/ui-sdk/workspace | CreatiqWorkspace |
@creatiq/ui-sdk/picker | ContentPickerSheet |
@creatiq/ui-sdk/collections | Collection components (list, detail, cards, modals) |
@creatiq/ui-sdk/ai | AI generation hooks and sidebar |
@creatiq/ui-sdk/browser | Standalone content browser components |
@creatiq/ui-sdk/styles.css | Required stylesheet |