Embed postMessage Protocol
What the embed SDK sends and receives, for anyone who builds the iframe by hand.
The embed SDK wraps this protocol. Use the protocol directly if you cannot add a script, for example in a native web view that frames Meet from your own page.
Before you start#
- Your page's origin must be in the project's Embed origins. Meet answers every room page with
Content-Security-Policy: frame-ancestors 'self' <your origins>, and the room speakspostMessageonly to a parent whose origin is on the list, to that origin and never to*. It takes commands only from the window that framed it.https://*.acme.commatches every subdomain,http://localhost:3000is for development, and*is refused for a project with live keys. - The iframe needs
allow="camera; microphone; display-capture; autoplay; fullscreen; clipboard-write; speaker-selection". - Screen sharing started by a command needs a click inside the frame first, because browsers require a user gesture there.
Address options#
| Option | Meaning |
|---|---|
?token= |
The meeting token |
?key= |
An invite key |
?theme=dark or light |
Colour theme |
?lang= |
Interface language |
?hide=chat,people,screen,pip,leave,header |
Remove those controls |
?parent=<origin> |
A hint for browsers without location.ancestorOrigins (the SDK adds it) |
Envelope#
Every message is { source: "meet", v: 1, kind: "command" | "response" | "event", type, data, id }.
- A command carries your own
id. - The response has
kind: "response", the sameidandtype, anddataon success orerror: { code, message }. - An event has no
id. - A command sent before the room has mounted waits up to 8 seconds.
const frame = document.querySelector('iframe');
const send = (type, data = {}) => frame.contentWindow.postMessage(
{ source: 'meet', v: 1, kind: 'command', type, data, id: crypto.randomUUID() }, 'https://meet.respiresoft.com');
window.addEventListener('message', (e) => {
if (e.origin !== 'https://meet.respiresoft.com' || e.data?.source !== 'meet') return;
console.log(e.data.kind, e.data.type, e.data.data ?? e.data.error);
});
send('join', { name: 'Ann', audio: true, video: true });Commands#
Command (data) |
Does | Errors |
|---|---|---|
join {name?, key?, lang?, audio?, video?} |
Enter the room and the call. Answers {pending} when the host must admit you. |
already_joined, name_required, join_failed |
leave |
Leave the call | |
setAudio {on}, setVideo {on} |
Microphone, camera | not_in_call, bad_argument |
startScreenShare, stopScreenShare |
Share the screen | not_in_call, unsupported |
setTheme {theme} |
dark or light |
bad_argument |
setCaptions {on} |
Captions on or off. on also starts caption events. |
bad_argument |
setLanguage {language} |
The language you listen in | bad_argument |
sendChat {text} |
A chat message, up to 2,000 characters | not_in_room, bad_argument |
An unknown command answers unknown_command. A command with no handler answers not_available.
Events#
Event (data) |
When |
|---|---|
ready {room: {slug, name}, state: "door" or "blocked"} |
The room page is up and your origin is allowed. Sent once. |
joined {participantId, name, role} |
You entered the call |
left {reason} |
left, kicked, revoked or ended |
participant-joined, participant-updated {id, name, audio, video, screen, hand} |
Someone joined, or a mic, camera, screen or hand changed (also you, for updates) |
participant-left {id, name} |
Someone left the call |
active-speaker {participantId} |
The loudest speaker changed. null when quiet. |
chat-message {id, participantId, name, text, ts, own} |
A new message. History is not replayed. |
caption {participantId, text, final, lang} |
Only after setCaptions with on: true |
error {code, message} |
room_closed, room_locked, room_notFound and other refused rooms, or call_error |
recording-started, recording-stopped |
Reserved. Not sent yet. |