Skip to main content
DocsIntegration GuidesEmbedding Integration
Back to docs

Embedding Integration

Iframe embedding, postMessage API, SDK setup

integration
embedding

Embedding Integration Guide

Overview

Creatiq provides two approaches for embedding H5P content into your application:

  1. Iframe Embed -- Load the Creatiq embed pages directly in an <iframe>. Works with any web framework or vanilla HTML. Requires constructing URLs and listening for postMessage events manually.

  2. SDK Embed -- Install @creatiq/ui-sdk and 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}
ParameterRequiredDescription
contentIdYesThe H5P content ID to display
tokenYesBearer authentication token
apiBaseNoOverride 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 TypePayloadDescription
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 TypePayloadDescription
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
ParameterRequiredDescription
contentIdYesContent ID to edit, or new for creation
tokenYesBearer authentication token
languageNoEditor 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 TypePayloadDescription
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:

  1. Content is sent to POST /h5p/content (new) or PUT /h5p/content/{contentId} (edit)
  2. A creatiq:saved message is posted to the parent with the resulting contentId and metadata

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

PropertyTypeRequiredDescription
baseUrlstringYesCreatiq API base URL
tokenstringYesSDK authentication token
locale'en' | 'tr' | 'fr' | 'es' | 'ar'NoUI locale
themePartial<CreatiqTheme>NoTheme overrides
aiCreatiqAIConfigNoAI feature configuration
onTokenExpired() => Promise<string>NoToken refresh callback
onLogout() => voidNoLogout callback
onLocaleChange(locale: string) => voidNoLocale change callback
tenantIdstringNoTenant identifier

CreatiqTheme Properties

PropertyCSS VariableDescription
colorPrimary--crq-color-primaryPrimary brand color
colorPrimaryForeground--crq-color-primary-foregroundText on primary color
colorBackground--crq-color-backgroundPage background
colorSurface--crq-color-surfaceCard/surface background
colorText--crq-color-textPrimary text color
colorMuted--crq-color-mutedMuted/secondary text
colorBorder--crq-color-borderBorder color
colorDestructive--crq-color-destructiveError/destructive color
borderRadius--crq-border-radiusGlobal border radius
fontFamily--crq-font-familyFont 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:

PropTypeDefaultDescription
contentIdstring--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
classNamestring--Wrapper div class
minHeightnumber200Minimum height in pixels
maxHeightnumber--Maximum height in pixels
fillContainerbooleanfalseUse 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:

PropTypeDefaultDescription
contentIdstring--Content ID to edit (omit for new content)
languagestringconfig localeEditor 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
classNamestring--Wrapper div class
heightnumber600Editor 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

  1. Your backend authenticates the user through your own auth system
  2. 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"
}
  1. 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 PathContents
@creatiq/ui-sdkProvider, hooks, player, editor, picker, workspace, types
@creatiq/ui-sdk/playerCreatiqPlayer, message protocol utilities
@creatiq/ui-sdk/editorCreatiqEditor
@creatiq/ui-sdk/workspaceCreatiqWorkspace
@creatiq/ui-sdk/pickerContentPickerSheet
@creatiq/ui-sdk/collectionsCollection components (list, detail, cards, modals)
@creatiq/ui-sdk/aiAI generation hooks and sidebar
@creatiq/ui-sdk/browserStandalone content browser components
@creatiq/ui-sdk/styles.cssRequired stylesheet
Back to docsdocs/product/integration/embedding.md