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

> 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]

Source: https://jitsi.help/guides/embed-jitsi-iframe-api/
Updated: October 6, 2026
Publisher: Jitsi Help (https://jitsi.help/)

## 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]

   ```ini
   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]

   ```sh
   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]

   ```sh
   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]

   ```sh
   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]

   ```js
   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]

   ```js
   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]

   ```js
   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]

   ```js
   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]

   ```js
   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]

```sh
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]

```js
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]

```sh
docker compose logs --tail=100 web prosody jicofo jvb
```

Debian/Ubuntu packages: [S17]

```sh
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](https://jitsi.github.io/handbook/docs/dev-guide/dev-guide-iframe/), checked 2026-10-06, official doc.

[S2] [API commands](https://jitsi.github.io/handbook/docs/dev-guide/dev-guide-iframe-commands/), checked 2026-10-06, official doc.

[S3] [API events](https://jitsi.github.io/handbook/docs/dev-guide/dev-guide-iframe-events/), official doc; [conference role dispatch](https://github.com/jitsi/jitsi-meet/blob/stable/jitsi-meet_11248/conference.js), [API middleware](https://github.com/jitsi/jitsi-meet/blob/stable/jitsi-meet_11248/react/features/external-api/middleware.ts), source code, checked 2026-10-06.

[S4] [API functions](https://jitsi.github.io/handbook/docs/dev-guide/dev-guide-iframe-functions/), checked 2026-10-06, official doc.

[S5] [External API, 11248](https://github.com/jitsi/jitsi-meet/blob/stable/jitsi-meet_11248/modules/API/external/external_api.js), checked 2026-10-06, source code.

[S6] [Configuration](https://github.com/jitsi/jitsi-meet/blob/stable/jitsi-meet_11248/config.js), [interface configuration](https://github.com/jitsi/jitsi-meet/blob/stable/jitsi-meet_11248/interface_config.js), [config whitelist](https://github.com/jitsi/jitsi-meet/blob/stable/jitsi-meet_11248/react/features/base/config/configWhitelist.ts), [interface whitelist](https://github.com/jitsi/jitsi-meet/blob/stable/jitsi-meet_11248/react/features/base/config/interfaceConfigWhitelist.ts), checked 2026-10-06, source code.

[S7] [Docker environment example](https://github.com/jitsi/docker-jitsi-meet/blob/stable-11248/env.example), [Compose](https://github.com/jitsi/docker-jitsi-meet/blob/stable-11248/docker-compose.yml), [Prosody template](https://github.com/jitsi/docker-jitsi-meet/blob/stable-11248/prosody/rootfs/defaults/conf.d/jitsi-meet.cfg.lua), checked 2026-10-06, source code.

[S8] [Token claims](https://github.com/jitsi/lib-jitsi-meet/blob/master/doc/tokens.md), official repo doc; [11248 validation](https://github.com/jitsi/jitsi-meet/blob/stable/jitsi-meet_11248/resources/prosody-plugins/token/util.lib.lua), source code, checked 2026-10-06.

[S9] [Package token handbook](https://jitsi.github.io/handbook/docs/devops-guide/token-authentication/), official doc; [token package postinst](https://github.com/jitsi/jitsi-meet/blob/stable/jitsi-meet_11248/debian/jitsi-meet-tokens.postinst), source code, checked 2026-10-06.

[S10] [Issue #17784](https://github.com/jitsi/jitsi-meet/issues/17784), community report and maintainer comments; [PR #17860](https://github.com/jitsi/jitsi-meet/pull/17860), proposed source change; [11248 lobby actions](https://github.com/jitsi/jitsi-meet/blob/stable/jitsi-meet_11248/react/features/lobby/actions.any.ts), source code, checked 2026-10-06.

[S11] MDN: [CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy), [script-src](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/script-src), [frame-src](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/frame-src), [frame-ancestors](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/frame-ancestors), [Permissions-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Permissions-Policy), [getUserMedia](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia), [getDisplayMedia](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia), [crossorigin](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/crossorigin), [secure contexts](https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Secure_Contexts#framed_documents), [X-Frame-Options](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Frame-Options), checked 2026-10-06, official browser docs.

[S12] [Docker Nginx template](https://github.com/jitsi/docker-jitsi-meet/blob/stable-11248/web/rootfs/defaults/meet.conf), [package postinst](https://github.com/jitsi/jitsi-meet/blob/stable/jitsi-meet_11248/debian/jitsi-meet-web-config.postinst), checked 2026-10-06, source code.

[S13] [Docker handbook](https://jitsi.github.io/handbook/docs/devops-guide/devops-guide-docker/), [package quickstart](https://jitsi.github.io/handbook/docs/devops-guide/devops-guide-quickstart/), checked 2026-10-06, official docs.

[S14] [Docker stable-11248](https://github.com/jitsi/docker-jitsi-meet/releases/tag/stable-11248), [Jitsi Meet 11248](https://github.com/jitsi/jitsi-meet/releases/tag/stable/jitsi-meet_11248), released 2026-09-14, release notes; latest releases checked 2026-10-06.

[S15] [curl manual](https://curl.se/docs/manpage.html), checked 2026-10-06, official doc.

[S16] [Nginx command options](https://nginx.org/en/docs/switches.html), [add_header](https://nginx.org/en/docs/http/ngx_http_headers_module.html#add_header), checked 2026-10-06, official docs.

[S17] [Compose CLI](https://docs.docker.com/reference/cli/docker/compose/), [systemctl](https://www.freedesktop.org/software/systemd/man/latest/systemctl.html), [journalctl](https://www.freedesktop.org/software/systemd/man/latest/journalctl.html), checked 2026-10-06, official docs.

[S18] [Token affiliation](https://github.com/jitsi/jitsi-meet/blob/stable/jitsi-meet_11248/resources/prosody-plugins/mod_token_affiliation.lua), [Jicofo roles](https://github.com/jitsi/jicofo/blob/stable/jitsi-meet_11248/jicofo/src/main/kotlin/org/jitsi/jicofo/xmpp/muc/ChatRoomRoleManager.kt), [defaults](https://github.com/jitsi/jicofo/blob/stable/jitsi-meet_11248/jicofo/src/main/resources/reference.conf), [launcher](https://github.com/jitsi/jicofo/blob/stable/jitsi-meet_11248/resources/jicofo.sh), [Docker template](https://github.com/jitsi/docker-jitsi-meet/blob/stable-11248/jicofo/rootfs/defaults/jicofo.conf), checked 2026-10-06, source code.

[S19] [Lobby password handling](https://github.com/jitsi/jitsi-meet/blob/stable/jitsi-meet_11248/resources/prosody-plugins/mod_muc_lobby_rooms.lua), 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]

---

Jitsi Help is an independent service. It is not affiliated with, endorsed by or sponsored by 8x8, Inc. or the Jitsi project. Jitsi and Jitsi Meet are trademarks of 8x8, Inc., used here only to describe the software we host and support.
