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:
| Missing | What the participant sees |
|---|---|
camera | getUserMedia rejects with NotAllowedError; no video, and the pre-join preview stays blank |
microphone | the same for audio — the classic "nobody can hear me" report |
display-capture | the screen-share button renders but silently does nothing |
autoplay | remote audio stays suspended until a click; the participant joins to silence |
fullscreen / allowfullscreen | the 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": {} }| Event | When | payload |
|---|---|---|
ready | the /join surface loaded (pre-join screen) | {} |
joined | the participant connected to the room | {} |
left | the connection ended, incl. a normal leave | { reason } |
error | an abnormal disconnect (also preceded by left) | { reason } |
participant.joined / participant.left | roster changes | { identity } |
recording.started / recording.stopped | recording 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.