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
userInfodoes not, by itself, implement host versus attendee permissions. [S8][S18]
Steps
-
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]ConfigENABLE_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]
Terminaldocker compose up -d --force-recreate -
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]
Terminalsudo apt-get install jitsi-meet-tokensIn the existing token VirtualHost in
/etc/prosody/conf.avail/meet.example.com.cfg.lua, retain the installer-createdauthentication = "token",app_id,app_secretand MUCtoken_verificationmodule. Add explicit restrictions: [S8][S9]Luaallow_empty_token = false; asap_accepted_issuers = { "my_jitsi_app_id" }; asap_accepted_audiences = { "jitsi" };In
/etc/jitsi/jicofo/jicofo.conf, ensurejicofo.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]Nginxadd_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]
Terminalsudo nginx -t && sudo systemctl reload nginx sudo systemctl restart prosody jicofo -
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]
JavaScriptconst 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 defaultsJWT_ENABLE_DOMAIN_VERIFICATIONto false. If you enable it, use your actual internal XMPP domain or tenant, oftenmeet.jitsi, rather than assuming the public hostname. Restrictroomto the authorized room. [S7][S8] -
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]
JavaScriptconst 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
videoConferenceJoinedfor readiness. A frame load alone cannot prove joining. Also calldispose()on component or route cleanup. [S3][S5] -
Both install types, 11248: connect host controls. Run each command from its button. Do not run the whole block on startup. [S2][S4]
JavaScriptapi.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; prefergetRoomsInfo(). Screen sharing still requires user interaction and browser consent. [S4][S11] -
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]
JavaScriptlet 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)andsetLobby(false). For audio moderation, a moderator can usetoggleModeration,muteEveryoneandaskToUnmute: [S2]JavaScriptapi.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 noisLobbyEnabled()orlobbyModeChanged. 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.comandframe-src https://meet.example.cominto the app’s CSP. Keep initialization in an allowed app script, or authorize inline code with a CSP nonce or hash. Jitsi separately needs compatibleframe-ancestors; inspect proxy-addedX-Frame-Options. Multiple CSP headers all apply. [S11] -
Media denied: the API already supplies
allowfor camera, microphone and display capture. A parent denial still wins. Where needed, merge this scoped parent response header: [S5][S11]httpPermissions-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 ininterfaceConfigOverwritecan fail. [S6] -
Unsupported API: the exact console message is
Not supported command name.CheckgetSupportedCommands()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
crossoriginattribute; 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]
curl --fail --silent --show-error --output /dev/null --write-out '%{http_code}\n' https://meet.example.com/external_api.jsIn the embedding page’s console, after creating api: [S4][S10]
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]
docker compose logs --tail=100 web prosody jicofo jvbDebian/Ubuntu packages: [S17]
sudo journalctl -u prosody -u jicofo -u jitsi-videobridge2 --no-pager -n 100If 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]