Skip to content

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
}
The legacy flat shape — a bare { 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

If you mount the iframe after it has already loaded, send walk:ping instead of racing the initial walk:loaded.

Inbound commands (host → iframe)

typepayloadEffect
walk:pingRe-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:resetRecenter the avatar and reset its pose.

Legacy command aliases (still accepted)

Legacy typeCanonical
walk:setEnvwalk:setEnvironment
walk:setMotionwalk:gesture
walk:narratewalk:say
walk:resetPosewalk:reset

Outbound events (iframe → host)

typepayloadWhen
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.

When you listen for events on the host side, always verify 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' }, '*');
Using the JavaScript SDK? It does the source check, channel guard, and event forwarding for you — just call ThreeWalkAvatar.on('walk:ready', …).