Skip to content

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, document fonouc_portal_user_inteface
  • Account override: Account database, document fonouc_portal_user_inteface, pvt_type: portal_user_interface

Runtime behavior (Portal → your site)

Implementation: portalfrontendExternal.jsx.

When a user opens the embed route, Portal:

  1. Loads iframe src = configured URL + query parameters (below)
  2. On iframe load and when working account changes, sends postMessage to 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:

  1. URL query account_id (available on first document load)
  2. FONOUC_AUTH_ACCESS_TOKEN.payload.account_id
  3. FONOUC_WORKING_ACCOUNT.payload.account_id

Recommended approach for embedded apps

  1. On first load: read account_id from URLSearchParams
  2. Register message listener for account switches (admin changes working account without closing iframe)
  3. Treat FONOUC_WORKING_ACCOUNT as the live update channel; use URL param for bootstrap only
  4. Use access_token only 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_TOKEN exposes the logged-in Portal user's JWT to the iframe origin. Only configure embed URLs you trust.
  • Validate event.origin in 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