Embedding in an iframe

Embed the hosted meeting UI and drive it over postMessage.

Embed /join — and only /join.

<iframe
  src="https://<meet-web-host>/join?token=<livekit-jwt>&url=<livekit-wss-url>"
  allow="camera; microphone; display-capture; autoplay; fullscreen"
  allowfullscreen
></iframe>

Never embed /call/*, /guest/*, or any dashboard route. Those authenticate with a session cookie, and in a third-party frame that cookie is partitioned into a separate jar or blocked outright — a login inside the frame either fails or lands somewhere the top-level site cannot reach. /join uses no cookies at all: the participant token in the URL is the credential. That is what makes it structurally immune to third-party cookie blocking, and it is the reason it is the only embeddable surface.

Permission delegation

The allow attribute is not optional — permissions must be delegated explicitly:

MissingWhat the participant sees
cameragetUserMedia rejects with NotAllowedError; no video, and the pre-join preview stays blank
microphonethe same for audio — the classic "nobody can hear me" report
display-capturethe screen-share button renders but silently does nothing
autoplayremote audio stays suspended until a click; the participant joins to silence
fullscreen / allowfullscreenthe fullscreen control no-ops

Two more requirements: the parent page must itself hold the permission (it cannot delegate what it does not have, so serve it over HTTPS and don't nest it under a restrictive policy), and the parent must not strip the URL fragment, or E2EE rooms lose their key.

Framing is not restricted. No X-Frame-Options and no CSP frame-ancestors are set on the Meet host, deliberately: one host serves every tenant, so an edge-level allowlist could only ever be one global list, and clickjacking is not the interesting attack when whoever embeds the page already holds the token. (Token leakage is — which is what the short TTL and Referrer-Policy: strict-origin-when-cross-origin address.)

postMessage events (optional, embedded only)

Append &parentOrigin=<your exact origin> (e.g. &parentOrigin=https://app.example.com) to the iframe src and the embedded page will post call lifecycle events to your page — and accept a small set of commands back. Without parentOrigin (or outside an iframe) the bridge is completely inert. Events are posted with your declared origin as the targetOrigin (never *), and commands are only accepted from that origin.

Outbound message shape (versioned; unknown fields and events are additive):

{ "source": "etite-meet", "v": 1, "event": "joined", "payload": {} }
EventWhenpayload
readythe /join surface loaded (pre-join screen){}
joinedthe participant connected to the room{}
leftthe connection ended, incl. a normal leave{ reason }
erroran abnormal disconnect (also preceded by left){ reason }
participant.joined / participant.leftroster changes{ identity }
recording.started / recording.stoppedrecording state{}

Commands you may post to the iframe (iframe.contentWindow.postMessage(msg, meetOrigin)):

{ "source": "etite-meet-host", "command": "leave" }

Supported commands: leave, toggleAudio, toggleVideo.

Listen safely: check event.origin is the Meet host and data.source === 'etite-meet' before trusting anything.

Per-tenant branding

When your tenant has a customDomain configured, tokens carry it as brandingHost metadata and the hosted page resolves your name/logo/color at runtime from the public GET /v1/branding?host= — the embedded UI wears your brand with zero build-time work. Ask your operator to set the tenant's custom domain, logo, and primary color.

On this page