How do I embed self-hosted Jitsi Meet with the IFrame API?

Short answer

Load external_api.js from your Jitsi server and create JitsiMeetExternalAPI with the room, user details and a backend-issued JWT. Use commands to control the meeting and events to update your app. On stable-11248, toggleLobby requires an explicit target state, but there is no lobby state getter or event, so local state cannot prove the server changed. [S1][S2][S10]

Who this is for

You want application login, host controls and meeting events inside your app. Docker stable-11248, released 2026-09-14, remains the latest release checked on 2026-10-06. [S1][S14]

How it works

external_api.js defines JitsiMeetExternalAPI, creates the iframe and transports commands and events across origins. Do not create another iframe around it. [S5]

A JWT carries authentication and room authorization to Prosody. Your backend signs it after authorizing the user. The browser receives the token, never the signing secret. Client display settings and hidden buttons do not enforce server permissions. [S8][S18]

Commands send requests. executeCommand() returns no confirmation. Some functions return Promises, including isAudioMuted() and getRoomsInfo(). Others, including getSupportedCommands(), return values immediately. [S4][S5]

Before you start

  • Confirm an ordinary meeting works on https://meet.example.com. The meeting, app and any outer embedding ancestors need HTTPS for media capture. [S11][S13]
  • Keep working DNS and firewall rules. Standard public ports are TCP 80 and 443, UDP 10000 for media. Docker’s sample host ports are 8000 and 8443. Embedding needs no extra Jitsi port. [S7][S13]
  • Choose your actual application origin. Examples use https://app.example.com; substitute yours everywhere. CSP checks origins, not just a shared parent domain. [S11]
  • Have a backend signer and a server policy that assigns the intended moderator roles. A valid JWT or userInfo does not, by itself, implement host versus attendee permissions. [S8][S18]

Steps

  1. Docker stable-11248: enable token authentication and framing. Edit .env. Privately replace the secret placeholder with your backend’s shared secret. This requires tokens from everyone. Preserve existing settings and CSP directives. [S7][S12]

    Config
    ENABLE_AUTH=1
    AUTH_TYPE=jwt
    ENABLE_GUESTS=0
    JWT_ALLOW_EMPTY=0
    JWT_APP_ID=my_jitsi_app_id
    JWT_APP_SECRET=REPLACE_WITH_A_PRIVATE_RANDOM_SECRET
    JWT_ACCEPTED_ISSUERS=my_jitsi_app_id
    JWT_ACCEPTED_AUDIENCES=jitsi
    ENABLE_LOBBY=1
    CSP_HEADER="frame-ancestors 'self' https://app.example.com"

    Recreate from the matching Compose directory to apply environment changes. [S7][S13][S17]

    Terminal
    docker compose up -d --force-recreate
  2. Debian/Ubuntu packages, Jitsi Meet 11248: configure tokens and framing. Install the token package and supply its Application ID and Application Secret when prompted. [S9]

    Terminal
    sudo apt-get install jitsi-meet-tokens

    In the existing token VirtualHost in /etc/prosody/conf.avail/meet.example.com.cfg.lua, retain the installer-created authentication = "token", app_id, app_secret and MUC token_verification module. Add explicit restrictions: [S8][S9]

    Lua
    allow_empty_token = false;
    asap_accepted_issuers = { "my_jitsi_app_id" };
    asap_accepted_audiences = { "jitsi" };

    In /etc/jitsi/jicofo/jicofo.conf, ensure jicofo.authentication.enabled = false, editing the existing value and preserving other settings. This follows the package token handbook. Docker generates different Jicofo authentication settings. [S9][S18]

    In /etc/nginx/sites-available/meet.example.com.conf, merge this into the HTTPS server block’s CSP: [S12][S16]

    Nginx
    add_header Content-Security-Policy "frame-ancestors 'self' https://app.example.com" always;

    Validate and reload Nginx, then restart Prosody and Jicofo during a maintenance window. [S16][S17]

    Terminal
    sudo nginx -t && sudo systemctl reload nginx
    sudo systemctl restart prosody jicofo
  3. Both install types, 11248: issue a short-lived JWT on the backend. Sign with HS256 and the configured application secret. This JavaScript object is the payload, not a complete signing implementation. Use your backend’s JWT library. [S8]

    JavaScript
    const payload = {
      iss: 'my_jitsi_app_id',
      aud: 'jitsi',
      sub: 'meet.example.com',
      room: 'team-standup',
      exp: Math.floor(Date.now() / 1000) + 300,
      context: { user: { id: 'user-42', name: 'Host', email: 'admin@example.com' } }
    };

    Packages normally verify the domain against sub. Docker defaults JWT_ENABLE_DOMAIN_VERIFICATION to false. If you enable it, use your actual internal XMPP domain or tenant, often meet.jitsi, rather than assuming the public hostname. Restrict room to the authorized room. [S7][S8]

  4. Both install types, 11248: load the API and initialize it. Put these elements in your app: [S1]

    HTML
    <script src="https://meet.example.com/external_api.js"></script>
    <div id="meet"></div>

    Run this in your app script after both elements exist. Replace the token placeholder. [S1][S5][S6]

    JavaScript
    const api = new JitsiMeetExternalAPI('meet.example.com', {
      roomName: 'team-standup',
      parentNode: document.querySelector('#meet'),
      width: '100%',
      height: 600,
      jwt: 'REPLACE_WITH_BACKEND_ISSUED_JWT',
      userInfo: { displayName: 'Host', email: 'admin@example.com' },
      configOverwrite: {
        startWithAudioMuted: true,
        prejoinConfig: { enabled: false },
        securityUi: { hideLobbyButton: true }
      },
      interfaceConfigOverwrite: { DISABLE_DOMINANT_SPEAKER_INDICATOR: true }
    });
    api.addListener('videoConferenceJoined', () => console.log('JOINED'));
    api.addListener('participantJoined', e => console.log('participantJoined', e.id));
    api.addListener('participantLeft', e => console.log('participantLeft', e.id));
    api.addListener('audioMuteStatusChanged', e => console.log('audio muted', e.muted));
    api.addListener('errorOccurred', e => console.error(e.type, e.name, e.message));
    api.addListener('readyToClose', () => api.dispose());

    Use videoConferenceJoined for readiness. A frame load alone cannot prove joining. Also call dispose() on component or route cleanup. [S3][S5]

  5. Both install types, 11248: connect host controls. Run each command from its button. Do not run the whole block on startup. [S2][S4]

    JavaScript
    api.executeCommand('toggleAudio');
    api.executeCommand('toggleVideo');
    api.executeCommand('toggleShareScreen');
    api.executeCommand('displayName', 'Host');
    api.executeCommand('sendChatMessage', 'Welcome');
    api.executeCommand('hangup');
    api.isAudioMuted().then(muted => console.log(muted));
    api.getRoomsInfo().then(info => console.log(info.rooms));

    getParticipantsInfo() is deprecated in the handbook; prefer getRoomsInfo(). Screen sharing still requires user interaction and browser consent. [S4][S11]

  6. Both install types, 11248: control lobby and moderation. Match role events to the local ID. The source also sends remote role events, contrary to the handbook’s local-only description. Store requested lobby state without claiming confirmation. [S2][S3][S10]

    JavaScript
    let localId = null;
    const roles = new Map();
    let requestedLobbyState = null;
    api.addListener('videoConferenceJoined', ({ id }) => {
      localId = id;
    });
    api.addListener('participantRoleChanged', ({ id, role }) => {
      roles.set(id, role);
    });
    api.addListener('videoConferenceLeft', () => {
      localId = null;
      roles.clear();
    });
    function setLobby(enabled) {
      if (roles.get(localId) !== 'moderator') return;
      api.executeCommand('toggleLobby', enabled);
      requestedLobbyState = enabled;
    }
    api.addListener('knockingParticipant', ({ participant }) => {
      if (roles.get(localId) !== 'moderator') return;
      const approved = window.confirm(`Admit ${participant.name}?`);
      api.executeCommand('answerKnockingParticipant', participant.id, approved);
    });

    Wire separate enable and disable buttons to setLobby(true) and setLobby(false). For audio moderation, a moderator can use toggleModeration, muteEveryone and askToUnmute: [S2]

    JavaScript
    api.executeCommand('toggleModeration', true, 'audio');
    api.executeCommand('muteEveryone', 'audio');
    // After selecting a participant from getRoomsInfo():
    api.executeCommand('askToUnmute', participantId);
    api.executeCommand('kickParticipant', participantId);

    In 11248, omitting toggleLobby’s argument disables the lobby. There is no isLobbyEnabled() or lobbyModeChanged. Issue #17784 and proposed PR #17860 remain open and unmerged, not fixed as of 2026-10-06. Another moderator can change the lobby, and enabling can be ignored when visitors are present. Hiding the local switch reduces accidental changes but cannot guarantee synchronized state. [S10]

Configuration reference

Defaults are from 11248; server overrides can change them. [S5][S6][S7][S8]

Name Where Default Purpose
roomName Constructor '' Room name. [S5]
parentNode; width; height Constructor document.body; '100%'; '100%' Placement and size. [S5]
jwt; userInfo Constructor Unset Token and display identity. [S5]
configOverwrite; interfaceConfigOverwrite Constructor {} Allowed client overrides. [S5][S6]
startWithAudioMuted; prejoinConfig.enabled Config overwrite false; true Initial mute and prejoin page. [S6]
securityUi.hideLobbyButton Config overwrite false Hide local lobby switch. [S6]
DISABLE_DOMINANT_SPEAKER_INDICATOR Interface overwrite false Hide speaker indicator. [S6]
ENABLE_AUTH; AUTH_TYPE Docker .env 0; internal Authentication mode. [S7]
ENABLE_GUESTS; JWT_ALLOW_EMPTY Docker .env 0; 0 Tokenless access. [S7]
JWT_APP_ID; JWT_APP_SECRET Docker .env Unset Application and signing secret. [S7]
JWT_ACCEPTED_ISSUERS; JWT_ACCEPTED_AUDIENCES Docker .env Unset; utility uses app ID and wildcard audience Restrict accepted claims. [S7][S8]
JWT_ENABLE_DOMAIN_VERIFICATION Docker .env false Check token domain. [S7]
ENABLE_LOBBY; CSP_HEADER Docker .env true; unset Lobby module and response CSP. [S7][S12]
authentication; app_id; app_secret; token_verification Package Prosody configuration Set by token installer Token authentication and room validation. [S9]
allow_empty_token; asap_accepted_issuers; asap_accepted_audiences Package Prosody configuration false; app ID; wildcard audience Token and claim restrictions. [S8]
jicofo.authentication.enabled Package jicofo.conf false Keep disabled for token setup. [S9][S18]
iss; aud; sub; room; exp; context.user JWT payload No generated values Issuer, audience, domain, room, expiry and optional identity. [S8]
script-src; frame-src App response CSP Depends on existing policy Permit API script and meeting frame. [S11]
frame-ancestors Jitsi response CSP No restriction from this directive when absent Approved embedding origins. [S11]
camera; microphone; display-capture Permissions Policy and iframe allow Browser policy; API delegates these Media capability, subject to consent. [S5][S11]

Common mistakes

  • Blank frame: merge script-src 'self' https://meet.example.com and frame-src https://meet.example.com into the app’s CSP. Keep initialization in an allowed app script, or authorize inline code with a CSP nonce or hash. Jitsi separately needs compatible frame-ancestors; inspect proxy-added X-Frame-Options. Multiple CSP headers all apply. [S11]

  • Media denied: the API already supplies allow for camera, microphone and display capture. A parent denial still wins. Where needed, merge this scoped parent response header: [S5][S11]

    http
    Permissions-Policy: camera=(self "https://meet.example.com"), microphone=(self "https://meet.example.com"), display-capture=(self "https://meet.example.com")
  • Overrides ignored: production builds filter override keys through whitelists. Keep modern configuration in configOverwrite; obsolete toolbar settings in interfaceConfigOverwrite can fail. [S6]

  • Unsupported API: the exact console message is Not supported command name. Check getSupportedCommands() and load the script from the same server as the meeting. [S5]

  • Cross-origin confusion: use the API rather than accessing the iframe DOM. The standard script tag needs no crossorigin attribute; adding one introduces CORS checks. Restrictive sandboxing can block scripts or media. [S1][S11]

Verify

Both install types: this checks script availability. Expected: 200, which alone cannot prove a working conference. [S1][S15]

Terminal
curl --fail --silent --show-error --output /dev/null --write-out '%{http_code}\n' https://meet.example.com/external_api.js

In the embedding page’s console, after creating api: [S4][S10]

JavaScript
console.log(api.getSupportedCommands().includes('toggleLobby'));
console.log(api.getSupportedEvents().includes('knockingParticipant'));
console.log(api.getSupportedEvents().includes('lobbyModeChanged'));
console.log(typeof api.isLobbyEnabled);

Expected for 11248: true, true, false, undefined. Joining prints the application’s JOINED. Test two users: roles, admission, denial, media and screen sharing. Capability checks cannot prove authorization. [S3][S10][S18]

If it still fails

Inspect the app console and the meeting frame’s console and network requests separately. Check CSP, permission failures and errorOccurred payloads. Decode tokens locally and compare claims and expiry without exposing them in shared logs. [S3][S8][S11]

Docker stable-11248: [S7][S17]

Terminal
docker compose logs --tail=100 web prosody jicofo jvb

Debian/Ubuntu packages: [S17]

Terminal
sudo journalctl -u prosody -u jicofo -u jitsi-videobridge2 --no-pager -n 100

If lobby enabling fails, the pinned client source includes the exact log Ignoring enable lobby request because there are visitors in the call already. A client command cannot override that guard. [S10]

FAQ

Does JWT automatically choose the meeting host?

No. Stock token validation checks access. Moderator assignment depends on Prosody modules and Jicofo role configuration; a moderator claim alone is insufficient. [S8][S18]

Can my app read the current lobby state?

Not through the examined 11248 API. PR #17860 proposes a getter and event, but remains unmerged as of 2026-10-06. [S10]

Can I use a room password with the lobby?

Yes, through the password command and passwordRequired event. A correct room password can bypass lobby admission in the examined implementation, so test the access policy you intend. [S2][S3][S19]

Can I embed this in React?

Yes, using Jitsi’s React SDK or this API in your component lifecycle. Dispose the instance on cleanup and avoid creating duplicate meeting frames. [S1][S5]

Sources

[S1] IFrame API handbook, checked 2026-10-06, official doc.

[S2] API commands, checked 2026-10-06, official doc.

[S3] API events, official doc; conference role dispatch, API middleware, source code, checked 2026-10-06.

[S4] API functions, checked 2026-10-06, official doc.

[S5] External API, 11248, checked 2026-10-06, source code.

[S6] Configuration, interface configuration, config whitelist, interface whitelist, checked 2026-10-06, source code.

[S7] Docker environment example, Compose, Prosody template, checked 2026-10-06, source code.

[S8] Token claims, official repo doc; 11248 validation, source code, checked 2026-10-06.

[S9] Package token handbook, official doc; token package postinst, source code, checked 2026-10-06.

[S10] Issue #17784, community report and maintainer comments; PR #17860, proposed source change; 11248 lobby actions, source code, checked 2026-10-06.

[S11] MDN: CSP, script-src, frame-src, frame-ancestors, Permissions-Policy, getUserMedia, getDisplayMedia, crossorigin, secure contexts, X-Frame-Options, checked 2026-10-06, official browser docs.

[S12] Docker Nginx template, package postinst, checked 2026-10-06, source code.

[S13] Docker handbook, package quickstart, checked 2026-10-06, official docs.

[S14] Docker stable-11248, Jitsi Meet 11248, released 2026-09-14, release notes; latest releases checked 2026-10-06.

[S15] curl manual, checked 2026-10-06, official doc.

[S16] Nginx command options, add_header, checked 2026-10-06, official docs.

[S17] Compose CLI, systemctl, journalctl, checked 2026-10-06, official docs.

[S18] Token affiliation, Jicofo roles, defaults, launcher, Docker template, checked 2026-10-06, source code.

[S19] Lobby password handling, checked 2026-10-06, source code.

Open questions

The complete flow needs real Docker, package and mobile browser tests, including role enforcement and changes by other moderators. The lobby PR has no confirmed release date. [S10][S18]

Recently updated