Docs / Walk / postMessage events
postMessage events
The host page and the /walk-embed iframe talk over
window.postMessage. This is the wire contract: the message
envelope, the commands you send in, the events the iframe sends back, and how
origins are handled.
The envelope
Every message is a plain object on the three-walk channel at protocol version 1:
{
channel: 'three-walk', // namespace guard — ignore anything else
v: 1, // protocol version
type: 'walk:moved', // see commands / events below
id: '', // optional; echoed on a command's result
// ...type-specific payload
}
{ type: 'walk:*' } with no channel/v — is still accepted inbound for backward compatibility with embeds shipped before this contract existed. New integrations should use the channelled envelope.
Handshake
- iframe → host:
walk:loadedfires once, when the avatar is ready. - host → iframe:
walk:ping(optional) makes the iframe re-emitwalk:loaded.
If you mount the iframe after it has already loaded, send walk:ping instead of racing the initial walk:loaded.
Inbound commands (host → iframe)
| type | payload | Effect |
|---|---|---|
walk:ping | — | Re-emit walk:loaded. |
walk:move | { x, y, run? } or { dir, meters? } | Analog vector (each axis −1…1) or a discrete step (dir: forward/back/left/right, meters 0…12). |
walk:gesture | { gesture } | Play idle, walk, run, wave, or jump. |
walk:say | { text, durationMs? } | Show a speech bubble. Text is capped at 280 chars; duration clamps to 800–20000 ms. |
walk:setEnvironment | { env } | Switch world: studio, void, beach, sunset, night, or grid. |
walk:setAvatar | { id } | Swap the avatar (UUID or GLB/VRM URL, ≤2048 chars). |
walk:config | { speed? } | Set walk speed multiplier; clamps to 0.3–3. |
walk:reset | — | Recenter the avatar and reset its pose. |
Legacy command aliases (still accepted)
| Legacy type | Canonical |
|---|---|
walk:setEnv | walk:setEnvironment |
walk:setMotion | walk:gesture |
walk:narrate | walk:say |
walk:resetPose | walk:reset |
Outbound events (iframe → host)
| type | payload | When |
|---|---|---|
walk:loaded | { avatar } | Once, when the avatar is ready. |
walk:moved | { x, z, yaw, motion } | As the avatar moves (also emitted as legacy walk:position). |
walk:spoke | { text } | After a walk:say command renders. |
walk:gestured | { gesture } | After a gesture plays. |
walk:environment | { env } | After the environment changes. |
walk:avatarChanged | { id } | After an avatar swap succeeds. |
walk:error | { error, code? } | On a load or command failure. |
Origin & security
The embed is designed to run on any origin (it ships
frame-ancestors *), so the host's origin cannot be allow-listed in
advance. The real authentication is the message source: the
iframe only accepts messages from its own parent window, and the host SDK only
accepts messages from the iframe it mounted. As a belt-and-suspenders filter,
a security-conscious integrator can pass an explicit
allowOrigins list to the embed when the exact origin is known.
event.source === iframe.contentWindow before trusting a message, and check data.channel === 'three-walk' (or that the bare type is a known walk:* event).
End-to-end example
const frame = document.querySelector('iframe');
// Listen for events from the iframe.
window.addEventListener('message', (e) => {
if (e.source !== frame.contentWindow) return; // source check
const msg = e.data;
if (!msg || msg.channel !== 'three-walk') return; // channel guard
if (msg.type === 'walk:loaded') {
// Avatar ready — start driving it.
frame.contentWindow.postMessage(
{ channel: 'three-walk', v: 1, type: 'walk:setEnvironment', env: 'night' }, '*');
frame.contentWindow.postMessage(
{ channel: 'three-walk', v: 1, type: 'walk:say', text: 'Welcome!' }, '*');
}
if (msg.type === 'walk:moved') {
console.log('moved', msg.x, msg.z, msg.yaw, msg.motion);
}
});
// If the frame may already be loaded, nudge it to re-announce.
frame.contentWindow.postMessage({ channel: 'three-walk', v: 1, type: 'walk:ping' }, '*');
ThreeWalkAvatar.on('walk:ready', …).