Embed API Reference

The <open-human> custom element is the fastest way to embed a photorealistic digital human into any webpage - no build step, no framework, no WebGL boilerplate. Drop in one script tag and one HTML element and the engine handles everything: asset loading, rendering, animation, and streaming.


Two Embedding Modes

OpenHuman ships two integration paths. Choose based on how much control you need:

Web ComponentJavaScript SDK
SetupOne <script> + one HTML tagnpm install @openhuman/sdk
ControlHTML attributes + postMessageFull programmatic API
FrameworkFramework-agnosticES module, tree-shakeable
IsolationShadow DOM - no CSS bleedShares your page's DOM
Best forCMS embeds, no-code tools, iframesReact / Vue / Svelte apps, custom pipelines

Quick Embed

The absolute minimum to render a character on your page:

index.html
<!-- 1. Load the embed script (once, anywhere in <head> or <body>) -->
<script type="module" src="https://cdn.openhuman.io/sdk/latest/embed.js"></script>
 
<!-- 2. Place the component wherever you need it -->
<open-human src="https://assets.openhuman.io/characters/aria.ohb" style="width: 480px; height: 640px; display: block;"></open-human>

That's it. The component bootstraps a WebGL 2.0 context inside a Shadow DOM, streams the .ohb bundle, and starts playing the default idle animation.


Attributes

All configuration is done via HTML attributes on the <open-human> element. Attributes can be changed at runtime with element.setAttribute() and the engine responds immediately.

Asset & Identity

AttributeTypeDefaultDescription
srcstring-URL to a .ohb character bundle (required)
namestring''Accessible name for the element (aria-label fallback)
<open-human src="https://assets.example.com/characters/aria.ohb" name="Aria, your AI assistant"></open-human>

Playback & Animation

AttributeTypeDefaultDescription
animationstring'idle'Name of the animation state to play on load
autoplaybooleantrueStart animating as soon as the bundle is ready
loopbooleantrueLoop the current animation clip
speednumber1.0Playback speed multiplier
<open-human src="./characters/aria.ohb" animation="idle" autoplay loop speed="1.0"></open-human>

Rendering Quality

AttributeTypeDefaultDescription
quality'high' | 'medium' | 'low''high'Rendering quality preset
fpsnumber60Target frame rate (use 30 for mobile)
shadowsbooleantrueEnable PCF soft shadow map
post-processbooleantrueEnable bloom, DoF, ACES tonemap, FXAA
sssbooleantrueEnable screen-space subsurface scattering
environmentstring'studio_warm'IBL lighting preset (see Environments)
exposurenumber1.0Renderer exposure value
<!-- High quality desktop embed -->
<open-human src="./aria.ohb" quality="high" fps="60" shadows post-process sss environment="studio_cool" exposure="1.1"></open-human>
 
<!-- Mobile-optimised embed -->
<open-human src="./aria.ohb" quality="low" fps="30" no-shadows no-post-process></open-human>

Boolean attributes follow HTML conventions: presence = true, absence = false. To disable a boolean feature, either omit the attribute or prefix it with no-: no-shadows, no-post-process, no-sss.


Camera

AttributeTypeDefaultDescription
fovnumber35Vertical field of view in degrees
camera-positionstring'0,1.65,0.8'Camera position as x,y,z in world space (metres)
camera-targetstring'0,1.55,0'Look-at target as x,y,z
orbitbooleanfalseEnable mouse/touch orbit control for the viewer
orbit-min-distancenumber0.4Minimum orbit zoom distance (metres)
orbit-max-distancenumber3.0Maximum orbit zoom distance
<!-- Portrait bust shot with orbit enabled -->
<open-human src="./aria.ohb" fov="35" camera-position="0,1.65,0.8" camera-target="0,1.55,0" orbit orbit-min-distance="0.5" orbit-max-distance="2.0"></open-human>
 
<!-- Full-body shot -->
<open-human src="./aria.ohb" fov="45" camera-position="0,0.9,2.2" camera-target="0,0.9,0"></open-human>

Streaming

AttributeTypeDefaultDescription
streaming-urlstring-WebSocket (wss://) or HTTP (https://) stream URL
streaming-mode'full' | 'facs''full''facs' for lip-sync-only streams
streaming-smoothingnumber0.7FACS exponential moving average α
streaming-buffernumber80Jitter buffer depth in ms
<open-human src="./aria.ohb" streaming-url="wss://tts-backend.example.com/lipsync" streaming-mode="facs" streaming-smoothing="0.7" streaming-buffer="80"></open-human>

Loading & Fallback

AttributeTypeDefaultDescription
posterstring-URL of an image shown while the bundle loads
loading'eager' | 'lazy''eager''lazy' defers loading until the element enters the viewport
backgroundstring'transparent'CSS colour behind the canvas (e.g. '#1a1a1a', 'transparent')
<open-human src="./aria.ohb" poster="./aria-poster.jpg" loading="lazy" background="#111111"></open-human>

Environments

The environment attribute sets the IBL (image-based lighting) preset used for all lighting and reflections.

Preset nameDescription
studio_warmSoft warm key light - default, flattering for skin
studio_coolCool daylight fill - clean, neutral look
studio_rimDramatic rim lighting - high contrast
outdoor_overcastSoft diffuse overcast sky
outdoor_goldenWarm golden hour directional sun
dark_cinematicLow-key dark background with a single spotlight
<open-human src="./aria.ohb" environment="dark_cinematic"></open-human>

JavaScript API (on the element)

Once the component is defined and ready, you can call methods directly on the DOM element to control it programmatically.

Getting a reference

const el = document.querySelector("open-human")
 
// Wait for the engine to be fully initialised
await el.ready // Promise<void> - resolves when character is loaded and first frame is rendered

Playback

// Play an animation state (immediate, no crossfade)
el.play("talk_neutral")
 
// Crossfade to a new state
el.crossFadeTo("idle", 0.4) // 400 ms crossfade
 
// Pause / resume the render loop
el.pause()
el.resume()

Morph targets

// Set individual FACS weights
el.setMorph("mouthSmileLeft", 0.8)
el.setMorph("mouthSmileRight", 0.8)
 
// Set multiple at once
el.setMorphs({ jawOpen: 0.4, browInnerUp: 0.3 })
 
// Apply a named expression preset
el.applyPreset("happy", 0.7)
 
// Animate to target weights
await el.animateMorphsTo({ mouthSmileLeft: 0, mouthSmileRight: 0 }, { duration: 0.4 })
 
// Reset all morphs
el.resetMorphs()

Camera

// Set camera position and target
el.setCameraPosition([0, 1.65, 0.8])
el.setCameraTarget([0, 1.55, 0])
el.setFOV(35)
 
// Enable orbit control at runtime
el.enableOrbit(true)

Renderer

el.setExposure(1.2)
el.setEnvironment("studio_cool")
el.setSSS(true)
el.setPostProcess(true)
el.setDOF({ focalDistance: 1.4, aperture: 0.04 })

Streaming

// Connect a stream
await el.connectStream("wss://tts-backend.example.com/lipsync", { mode: "facs" })
 
// Disconnect
el.disconnectStream()
 
// Stream stats
const stats = el.getStreamStats()
console.log(stats.bufferDepth, stats.estimatedLatency, stats.fps)

Lifecycle

// Tear down the engine and release all GPU resources
el.destroy()

Events

The <open-human> element dispatches standard CustomEvent instances on itself. Listen with addEventListener:

const el = document.querySelector("open-human")
 
// Engine lifecycle
el.addEventListener("oh:ready", (e) => console.log("Character ready"))
el.addEventListener("oh:error", (e) => console.error("Error:", e.detail.message))
el.addEventListener("oh:loadprogress", (e) => console.log(`${e.detail.stage}: ${e.detail.percent}%`))
 
// Animation
el.addEventListener("oh:animationstart", (e) => console.log("Started:", e.detail.state))
el.addEventListener("oh:animationend", (e) => console.log("Ended:", e.detail.clip))
el.addEventListener("oh:transition", (e) => console.log(`${e.detail.from} → ${e.detail.to}`))
 
// Streaming
el.addEventListener("oh:streamconnected", () => console.log("Stream connected"))
el.addEventListener("oh:streamdisconnected", (e) => console.warn("Stream dropped:", e.detail.reason))
el.addEventListener("oh:streamframe", (e) => {
    // e.detail: { timestamp, joints?, facs? }
})
 
// User interaction (requires orbit="true")
el.addEventListener("oh:orbitstart", () => console.log("User started rotating"))
el.addEventListener("oh:orbitend", () => console.log("User stopped rotating"))

Event detail shapes

Evente.detail fields
oh:ready{ webglVersion: number, character: string }
oh:error{ code: string, message: string }
oh:loadprogress{ stage: string, percent: number }
oh:animationstart{ state: string, clip: string }
oh:animationend{ clip: string, loop: boolean }
oh:transition{ from: string, to: string, duration: number }
oh:streamconnected{ url: string, transport: string }
oh:streamdisconnected{ url: string, code: number, reason: string }
oh:streamframe{ timestamp: number, joints?: Float32Array, facs?: Float32Array }

postMessage API (iframe embed)

For maximum isolation - or when embedding inside a no-script CMS - serve the component inside an <iframe> and communicate via postMessage. The embed page at https://cdn.openhuman.io/embed/ accepts all the same commands.

Basic iframe setup

index.html
<iframe
    id="oh-frame"
    src="https://cdn.openhuman.io/embed/?src=https://assets.example.com/aria.ohb&quality=high&animation=idle"
    width="480"
    height="640"
    frameborder="0"
    allow="accelerometer; fullscreen"
></iframe>

Sending commands

const frame = document.getElementById("oh-frame").contentWindow
 
// Play an animation
frame.postMessage({ type: "oh:play", state: "talk_neutral" }, "*")
 
// Crossfade
frame.postMessage({ type: "oh:crossFadeTo", state: "idle", duration: 0.4 }, "*")
 
// Set morph targets
frame.postMessage({ type: "oh:setMorphs", weights: { jawOpen: 0.5, mouthFunnel: 0.3 } }, "*")
 
// Apply preset
frame.postMessage({ type: "oh:applyPreset", name: "happy", intensity: 0.8 }, "*")
 
// Connect a stream
frame.postMessage({ type: "oh:connectStream", url: "wss://...", mode: "facs" }, "*")
 
// Update camera
frame.postMessage({ type: "oh:setCamera", position: [0, 1.65, 0.8], target: [0, 1.55, 0] }, "*")

Receiving events from the iframe

window.addEventListener("message", (e) => {
    if (e.origin !== "https://cdn.openhuman.io") return // always check origin
    const { type, detail } = e.data
 
    switch (type) {
        case "oh:ready":
            console.log("Character loaded:", detail.character)
            break
        case "oh:animationend":
            console.log("Clip finished:", detail.clip)
            break
        case "oh:error":
            console.error("Engine error:", detail.message)
            break
    }
})

Supported postMessage commands

typePayload fieldsDescription
oh:playstate: stringPlay a state immediately
oh:crossFadeTostate: string, duration: numberCrossfade to a state
oh:pause-Pause the render loop
oh:resume-Resume the render loop
oh:setMorphtarget: string, weight: numberSet a single morph weight
oh:setMorphsweights: Record<string, number>Set multiple morph weights
oh:applyPresetname: string, intensity?: numberApply an expression preset
oh:resetMorphs-Reset all morphs to neutral
oh:connectStreamurl: string, mode?: stringConnect an animation stream
oh:disconnectStream-Disconnect the stream
oh:setCameraposition: number[], target: number[], fov?: numberUpdate camera
oh:setEnvironmentname: stringChange IBL lighting preset
oh:setExposurevalue: numberSet renderer exposure
oh:setQualityquality: stringSwitch quality preset at runtime
oh:destroy-Tear down the engine

React Integration

components/Avatar.tsx
import { useEffect, useRef } from "react"
 
// Load the web component definition once at app boot
import "@openhuman/sdk/embed"
 
interface AvatarProps {
    src: string
    animation?: string
    streamUrl?: string
    quality?: "high" | "medium" | "low"
    onReady?: () => void
    onError?: (code: string, message: string) => void
}
 
export function Avatar({ src, animation = "idle", streamUrl, quality = "high", onReady, onError }: AvatarProps) {
    const ref = useRef<HTMLElement>(null)
 
    useEffect(() => {
        const el = ref.current
        if (!el) return
 
        const handleReady = () => onReady?.()
        const handleError = (e: Event) => {
            const { code, message } = (e as CustomEvent).detail
            onError?.(code, message)
        }
 
        el.addEventListener("oh:ready", handleReady)
        el.addEventListener("oh:error", handleError)
 
        return () => {
            el.removeEventListener("oh:ready", handleReady)
            el.removeEventListener("oh:error", handleError)
        }
    }, [onReady, onError])
 
    return (
        <open-human
            ref={ref}
            src={src}
            animation={animation}
            quality={quality}
            streaming-url={streamUrl}
            streaming-mode="facs"
            style={{ width: "100%", height: "100%", display: "block" }}
        />
    )
}

TypeScript users should add a declaration for the custom element to avoid JSX.IntrinsicElements type errors:

open-human.d.ts
declare namespace JSX {
    interface IntrinsicElements {
        "open-human": React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & {
            src?: string
            animation?: string
            quality?: "high" | "medium" | "low"
            autoplay?: boolean
            loop?: boolean
            orbit?: boolean
            environment?: string
            exposure?: number
            fov?: number
            "camera-position"?: string
            "camera-target"?: string
            "streaming-url"?: string
            "streaming-mode"?: "full" | "facs"
            "streaming-smoothing"?: number
            "streaming-buffer"?: number
            poster?: string
            loading?: "eager" | "lazy"
            background?: string
            shadows?: boolean
            "post-process"?: boolean
            sss?: boolean
            fps?: number
        }
    }
}

Vue Integration

components/Avatar.vue
<template>
    <open-human
        ref="el"
        :src="src"
        :animation="animation"
        :quality="quality"
        :streaming-url="streamUrl"
        streaming-mode="facs"
        style="width: 100%; height: 100%; display: block"
        @oh:ready="emit('ready')"
        @oh:error="onError"
    />
</template>
 
<script setup lang="ts">
import { ref } from "vue"
import "@openhuman/sdk/embed"
 
const props = defineProps<{ src: string; animation?: string; quality?: string; streamUrl?: string }>()
const emit = defineEmits<{ ready: []; error: [code: string, message: string] }>()
const el = ref<HTMLElement>()
 
const onError = (e: CustomEvent) => emit("error", e.detail.code, e.detail.message)
 
// Expose element methods to parent via template ref
defineExpose({
    play: (state: string) => el.value?.play(state),
    setMorphs: (w: Record<string, number>) => el.value?.setMorphs(w),
    applyPreset: (name: string, intensity = 1) => el.value?.applyPreset(name, intensity),
    connectStream: (url: string) => el.value?.connectStream(url),
})
</script>

Content Security Policy

If your site uses a CSP header, add the following directives to allow the engine to run:

Content-Security-Policy:
  script-src   'self' https://cdn.openhuman.io;
  connect-src  'self' https://assets.openhuman.io wss: https:;
  worker-src   blob:;
  img-src      'self' data: blob:;

The engine uses OffscreenCanvas and SharedArrayBuffer internally where available. These require the Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp headers on pages that host the component directly (not required for the iframe embed path).


Attribute Quick Reference

A flat reference of every supported attribute on <open-human>:

AttributeTypeDefault
srcstring- (required)
namestring''
animationstring'idle'
autoplaybooleantrue
loopbooleantrue
speednumber1.0
quality'high' | 'medium' | 'low''high'
fpsnumber60
shadowsbooleantrue
post-processbooleantrue
sssbooleantrue
environmentstring'studio_warm'
exposurenumber1.0
fovnumber35
camera-positionstring'0,1.65,0.8'
camera-targetstring'0,1.55,0'
orbitbooleanfalse
orbit-min-distancenumber0.4
orbit-max-distancenumber3.0
streaming-urlstring-
streaming-mode'full' | 'facs''full'
streaming-smoothingnumber0.7
streaming-buffernumber80
posterstring-
loading'eager' | 'lazy''eager'
backgroundstring'transparent'

Troubleshooting

SymptomLikely causeFix
Blank canvas, no errorWebGL 2.0 not supportedAdd a poster fallback image; check browser compatibility
oh:error with code BUNDLE_FETCH_FAILEDCORS on .ohb URLAdd Access-Control-Allow-Origin: * to your asset server
oh:error with code WEBGL2_NOT_SUPPORTEDOld browser or GPU blacklistedSet quality="low" to attempt WebGL 1.0 fallback
Component renders but no animation playsanimation attribute names a clip not in the bundleCheck animations/index.json inside the .ohb for valid clip names
Stream connects but mouth does not movestreaming-mode is 'full' but server sends FACS-only framesSet streaming-mode="facs"
High memory on mobileQuality too high for device GPUSet quality="low" and fps="30"
CSP blocks scriptcdn.openhuman.io not in script-srcAdd it to your CSP (see Content Security Policy)

Next Steps