Skip to content
PuniDocs
Open Puni

The embed protocol


The message API a host page can opt into: connect to the player, inspect controls, set a parameter, seek, play, pause, trigger and reset.

An embedded player can take instructions from the page around it, over postMessage. The host opts in by naming its own origin; the player answers only that origin, and only after a connect.

#Opt in

js
const url = new URL(sharedSceneUrl);
url.searchParams.set("embed", "1");
url.searchParams.set("parentOrigin", location.origin);
iframe.src = url.href;

Without both parameters there is no host API. The origin must be exact: scheme, host and port, no path.

#Connect

text
iframe.contentWindow.postMessage(
  { protocol: "puni.embed", version: 1, method: "connect", requestId: crypto.randomUUID() },
  url.origin
);

Check event.origin and event.source === iframe.contentWindow on every reply. The connect reply carries a channel; send it, with an increasing sequence, on every later request. A reload gets a new channel.

#Methods

MethodFieldsDoes
inspectoptional instanceIdThe top-level objects and response targets, or one object's controls with their current values
set_parameterinstanceId, property, valueChanges a control through the same validation as the editor; pauses playback
seektimePauses and moves to a time inside the duration
play, pausenonePlayback; play is refused under reduced motion or while hidden
triggertargetIdPlays a response on a visible target
resetnoneBack to the shared document, paused at zero, muted

Changes are local to the player. Nothing is saved, and the link's snapshot never changes.

#Events

After connecting, the player sends event: "state" when readiness, time, playback, mute or reduced motion change, at most four times a second, plus an immediate sample after each command, and event: "response" when a response plays.

#Limits and errors

Twenty requests per second. Replies that complete are cached by request ID (the last 64), so a retry of the same body cannot apply twice; rate-limit, schema, not_ready, reduced_motion and hidden failures are not cached, so a retry re-executes; a different body under the same ID fails with request_conflict. Expect invalid_request, invalid_operation, request_conflict, stale_request, stale_channel, not_ready, rate_limited, reduced_motion and hidden.

Only unlocked, visible top-level objects and their ordinary properties are exposed. There is no path from a host page to commands, selection, assets, or your account.

Read Embedding for the iframe itself.

Was this useful?