Portal Embedded Site Integration¶
Embed an external HTTPS application (CRM dashboard, partner portal, custom reporting site) inside the fonoUC Admin Portal sidebar. Portal loads your site in an iframe and passes working-account context so the embedded app can scope data per tenant.
This is the inverse of Web Embedded Soft Client (UCP embedded in your website). For agent-desktop CRM tabs in UCP, see UCP CRM Iframe Communication and CRM Home URL under Configuration → Autoprovision → UCP.
Overview¶
| Concept | Description |
|---|---|
| Portal embed | External URL shown as a sidebar item in Reports, Provision, Status, Users, or Contact Center |
| Global config | Default embed for all accounts (edited while working account is admin) |
| Account override | Per-account URL/title/position; overrides global for that account only |
| Working account | Account selected in the Portal top bar; drives X-Account-ID and iframe context |
Minimum platform versions
| Component | Version | Feature |
|---|---|---|
| goportalbackend | 2.119.9+ | Per-account portal user interface scope |
| portalfrontend | 1.118.5+ | Scope indicator, inherit/override UX |
| portalfrontend | 1.118.6+ (develop) | account_id on iframe URL + postMessage contract (see below) |
Admin configuration¶
Path: Configuration → Account → User Interface → Embed
Global configuration¶
When the working account is the master admin account:
- Banner: Global configuration
- Save applies to every account that does not define its own override
- Delete removes the global embed entirely
Per-account configuration¶
When the working account is any sub-account:
- Banner: Account configuration — {account name}
- If the account has no override: form shows inherited global values; banner explains that saving creates an override
- Remove override deletes only the account document; the account falls back to global
- Delete is hidden while still inheriting (nothing to remove yet)
Form fields¶
| Field | Required | Notes |
|---|---|---|
| URL | Yes | Must be HTTPS in production; loaded in iframe src |
| Username / Password | No | Sent to iframe via postMessage (see Auth) |
| Position | Yes | Sidebar section where the link appears |
| Title | Yes | Sidebar label |
Position values
| Value | Sidebar section |
|---|---|
sidebar_reports |
Reports |
sidebar_provision |
Provisioning |
sidebar_status |
Status |
sidebar_users |
Users |
sidebar_contact_center |
Contact Center |
Backend API (admin)¶
All requests use the Portal session Authorization header and X-Account-ID (working account).
Base path: /api/v2/config/globalconfig/portal-user-interface
| Method | Scope behavior |
|---|---|
| GET | Returns effective embed + scope (global | account) + inherited (bool) |
| PUT | Writes global doc (admin working account) or account override (sub-account) |
| DELETE | Deletes global doc or account override |
GET response (example)
{
"embed": {
"position": "sidebar_reports",
"url": "https://crm.example.com/portal",
"title": "CRM Dashboard",
"auth": { "username": "", "password": "" }
},
"scope": "account",
"inherited": false,
"goportalbackend_ai_assistant_portal_enabled": true
}
Storage
- Global: CouchDB
fonouc_global_config, documentfonouc_portal_user_inteface - Account override: Account database, document
fonouc_portal_user_inteface,pvt_type:portal_user_interface
Runtime behavior (Portal → your site)¶
Implementation: portalfrontend → External.jsx.
When a user opens the embed route, Portal:
- Loads iframe
src= configured URL + query parameters (below) - On iframe
loadand when working account changes, sendspostMessageto the embed origin
Query string parameters (iframe src)¶
Portal appends parameters to the configured URL:
| Parameter | Always | Description |
|---|---|---|
account_id |
When working account is set | Kazoo account ID (32-char hex) |
avoid_cache |
Yes | Timestamp; busts CDN/browser cache on navigation |
Examples
https://crm.example.com/app?account_id=abc123...&avoid_cache=1719172800123
https://crm.example.com/app?foo=bar&account_id=abc123...&avoid_cache=1719172800123
If the URL is not parseable as a full URL, Portal falls back to manual query concatenation with proper encoding.
postMessage — Portal → embedded site¶
Target origin: new URL(embedUrl).origin (not the full path).
Listen in your embedded app:
const PORTAL_ORIGIN = 'https://portal.example.com' // your Portal host
window.addEventListener('message', (event) => {
if (event.origin !== PORTAL_ORIGIN) return
const { type, payload } = event.data || {}
switch (type) {
case 'FONOUC_AUTH_ACCESS_TOKEN':
// payload.access_token — Portal JWT
// payload.account_id — working account ID
break
case 'FONOUC_EXTERNAL_SITE_AUTH':
// payload.username, payload.password — optional basic-auth hints from config
break
case 'FONOUC_WORKING_ACCOUNT':
// payload.account_id — working account ID (account switch without full reload)
break
}
})
Message reference¶
| type | payload | When sent |
|---|---|---|
FONOUC_AUTH_ACCESS_TOKEN |
{ access_token, account_id } |
iframe load; working account change |
FONOUC_EXTERNAL_SITE_AUTH |
{ username, password } |
iframe load; working account change |
FONOUC_WORKING_ACCOUNT |
{ account_id } |
iframe load; working account change |
Canonical account context (integrator guidance)¶
Portal exposes working account ID in three places:
- URL query
account_id(available on first document load) FONOUC_AUTH_ACCESS_TOKEN.payload.account_idFONOUC_WORKING_ACCOUNT.payload.account_id
Recommended approach for embedded apps
- On first load: read
account_idfromURLSearchParams - Register
messagelistener for account switches (admin changes working account without closing iframe) - Treat
FONOUC_WORKING_ACCOUNTas the live update channel; use URL param for bootstrap only - Use
access_tokenonly if your backend validates Portal JWTs; do not rely on it for tenant ID alone
All three account_id values should match; if they diverge, prefer the latest FONOUC_WORKING_ACCOUNT or reload the iframe.
Distinction: three embed/integration paths¶
| Feature | Config location | Host | Consumer |
|---|---|---|---|
| Portal sidebar embed (this doc) | Configuration → User Interface → Embed | Portal iframe | Admins / portal users with sidebar access |
| UCP CRM Home URL | Configuration → Autoprovision → UCP | UCP CRM panel | Call-center agents |
| Web Embedded Soft Client | Your website iframe | Your site embeds UCP | End users on partner sites |
Do not confuse Portal embed URL with UCP CRM Home URL; they use different APIs and storage.
Security considerations¶
- Embed URL and optional username/password are stored in CouchDB; restrict embed admin to trusted admins.
FONOUC_AUTH_ACCESS_TOKENexposes the logged-in Portal user's JWT to the iframe origin. Only configure embed URLs you trust.- Validate
event.originin your embedded app against the known Portal hostname(s). - Prefer HTTPS for embed URLs; mixed content will be blocked by browsers.
- Optional config username/password are sent in cleartext via
postMessage; use only for legacy basic-auth bridges, not as primary secrets.
Implementing a minimal embedded CRM page¶
<!DOCTYPE html>
<html>
<head>
<title>CRM for Portal embed</title>
</head>
<body>
# Loading…
<script>
const PORTAL_ORIGIN = 'https://portal.lab.example.com'
function setAccount(id) {
document.getElementById('account').textContent =
id ? `Account: ${id}` : 'No account'
// Fetch tenant-scoped data from your API using account_id
}
// Bootstrap from URL
setAccount(new URLSearchParams(location.search).get('account_id'))
// Live updates from Portal
window.addEventListener('message', (e) => {
if (e.origin !== PORTAL_ORIGIN) return
if (e.data?.type === 'FONOUC_WORKING_ACCOUNT') {
setAccount(e.data.payload?.account_id)
}
if (e.data?.type === 'FONOUC_AUTH_ACCESS_TOKEN') {
setAccount(e.data.payload?.account_id)
// optional: call your API with e.data.payload.access_token
}
})
</script>
</body>
</html>
Troubleshooting¶
| Symptom | Likely cause |
|---|---|
| Sidebar link missing | No embed configured, or position does not match section; globalconfig GET failed |
| Blank iframe | Invalid URL, X-Frame-Options / CSP frame-ancestors blocking Portal |
| Wrong tenant data | Embedded app ignoring account_id or not handling account-switch messages |
| Auth not received | Origin mismatch in your listener; check Portal hostname vs event.origin |
| Sub-account shows global URL | Expected when inheriting; save override or check account doc in CouchDB |
Related documentation¶
- UCP CRM Iframe Communication — messages from CRM iframe to UCP
- Web Embedded Soft Client — embed UCP in an external website