Skip to content

Embed via iframe (SSO)

The simplest embedding option for a legacy CRM whose frontend source you cannot modify, or for a partner who just wants to "hang" the Zorio webphone in their app without controlling it. The iframe ships a full UI (dialer, incoming popup, in-call panel) — drop in <iframe src=...> and you are done.

1. Minimal syntax

html
<iframe
  src="https://app.zorio.vn/webphone-embed?token=<jwt>"
  allow="microphone"
  style="border:0; width:380px; height:560px"
></iframe>

Custom domain

Replace app.zorio.vn with your custom domain if the partner has been provisioned a custom domain (e.g. pbx.your-crm.example). The iframe is always hosted on the domain assigned to the customer so that the bearer token / SIP credentials do not cross account boundaries.

2. SSO exchange — obtaining <jwt>

The JWT passed via the query string is a short-lived embed token (TTL 5 minutes by default), exchanged by the partner backend from the partner's user session via this API:

POST https://acme.zorio.example/api/webphone/embed-token
Authorization: Bearer <bearer-token-from-/api/auth/login>
Content-Type: application/json

{
  "ttl_seconds": 300
}

Response:

json
{
  "embed_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "expires_at": "2026-06-30T10:35:00+07:00"
}

Never expose your main bearer token

Do not put the original bearer token into the iframe src. A long-lived bearer leaked via a URL ends up in Referer headers and proxy logs. Always exchange it for a short-TTL embed token before injecting it into the iframe.

Typical flow

text
[Partner CRM]                  [Partner backend]            [Zorio API]
       │                              │                            │
       │  needs to render the         │                            │
       │  webphone                    │                            │
       ├─────────────────────────────►│                            │
       │                              │  POST /webphone/embed-token│
       │                              ├───────────────────────────►│
       │                              │                            │
       │                              │◄───────────────────────────┤
       │                              │  embed_token (TTL 5 min)   │
       │◄─────────────────────────────┤                            │
       │  embed_token                 │                            │
       │                              │                            │
       │  <iframe src="?token=...">                                 │
       │  → mount + SIP register      │                            │

When the token nears expiry, the parent frame can fetch a new one and forward it via postMessage (see postMessage API).

3. Full query string options

html
<iframe
  src="https://app.zorio.vn/webphone-embed
        ?token=<jwt>
        &layout=docked
        &theme=auto
        &locale=vi
        &auto_register=1"
  allow="microphone"
  style="border:0; width:380px; height:560px"
></iframe>
ParameterValueDescription
tokenembed JWTRequired — short-lived embed token
layoutdocked | overlay | fullscreenUI layout. Default: docked
themelight | dark | autoDefault: auto
localevi | enDefault: vi
auto_register1 | 0Auto-register SIP after load. Default: 1

4. Microphone permission on the iframe

You MUST add allow="microphone" — without it the browser will block getUserMedia even when the user has already granted permission to the parent page:

html
<iframe allow="microphone" ... ></iframe>

<!-- If you also need camera (video, Phase 2): -->
<iframe allow="microphone; camera" ... ></iframe>

Cross-origin

The app.zorio.vn iframe is always cross-origin with the partner CRM → all communication between parent and iframe must go through postMessage (see postMessage API). Do not try to access iframe.contentWindow.phone directly — the browser will block it.

5. postMessage API

The webphone iframe communicates with the parent via window.postMessage. Every message uses the same shape:

ts
interface ZorioWebphoneMessage {
  source: 'zorio-webphone'   // marker, ignore other messages
  type:   string             // event name or command
  payload?: any              // accompanying data
}

5.1 Listening to events from the iframe (iframe → parent)

js
window.addEventListener('message', (e) => {
  if (e.origin !== 'https://app.zorio.vn') return       // verify origin
  if (e.data?.source !== 'zorio-webphone') return       // verify marker

  switch (e.data.type) {
    case 'registered':
      console.log('Webphone is ready')
      break
    case 'incoming_call':
      console.log('Incoming call:', e.data.payload.call.number)
      myUI.showIncomingBanner(e.data.payload)
      break
    case 'call_answered':
      myUI.openCallControls(e.data.payload.call.uuid)
      break
    case 'call_hangup':
      myUI.closeCallControls()
      console.log('Billsec:', e.data.payload.billsec)
      break
    case 'error':
      console.error('Webphone error:', e.data.payload)
      break
  }
})

The iframe forwards all 12 events just like the npm/UMD SDK — see Events.

5.2 Controlling the webphone (parent → iframe)

js
const iframe = document.querySelector('iframe')

function sendCommand(type, payload) {
  iframe.contentWindow.postMessage(
    { source: 'zorio-webphone', type, payload },
    'https://app.zorio.vn'  // target origin — DO NOT use '*'
  )
}

// Click-to-call from a button inside the CRM
sendCommand('call', {
  number: '0912345678',
  options: { campaignId: 36, leadId: 78912 },
})

// Hangup the active call
sendCommand('hangup', {})

// Send DTMF
sendCommand('send_dtmf', { digits: '1' })

// Mute / unmute
sendCommand('mute', {})
sendCommand('unmute', {})

// Refresh a token nearing expiry
sendCommand('refresh_token', { token: newEmbedToken })

Command type values map 1-to-1 with the 12 API methods.

5.3 Full integration example

html
<div id="phone-container">
  <iframe
    id="zorio-frame"
    src="https://app.zorio.vn/webphone-embed?token=PLACEHOLDER"
    allow="microphone"
    style="border:0; width:380px; height:560px"
  ></iframe>
</div>

<button id="btn-call">Call 0912345678</button>

<script>
const ZORIO_ORIGIN = 'https://app.zorio.vn'
const iframe = document.getElementById('zorio-frame')

// 1. Fetch the embed token then inject
fetch('/api/internal/zorio-embed-token').then(r => r.json()).then(({ token }) => {
  iframe.src = `${ZORIO_ORIGIN}/webphone-embed?token=${token}&layout=docked`
})

// 2. Listen for events
window.addEventListener('message', (e) => {
  if (e.origin !== ZORIO_ORIGIN) return
  if (e.data?.source !== 'zorio-webphone') return
  console.log('[zorio]', e.data.type, e.data.payload)
})

// 3. Control
document.getElementById('btn-call').onclick = () => {
  iframe.contentWindow.postMessage(
    { source: 'zorio-webphone', type: 'call', payload: { number: '0912345678' } },
    ZORIO_ORIGIN
  )
}
</script>

6. v7 features still work inside the iframe

The iframe is built from the same SDK bundle, so it carries everything:

  • Auto-reconnect supervisor — no reconnect handling needed in the parent.
  • Pagehide cleanup — when the user closes the CRM tab, the iframe unregisters cleanly.
  • Early media — outbound calls hear ringback from SIP 183 Session Progress.

7. iframe vs SDK npm/UMD — when to use which?

CriterioniframeSDK npm/UMD
Integration speedFastest (one <iframe> tag)Medium
Custom UIStuck with the default Zorio UIFull control
Cross-originFully isolatedRequires CSP setup
Advanced event customizationpostMessageNative callbacks
Air-gapSelf-host the iframe internallySelf-host UMD

Recommendation

If the partner just needs "a webphone on the CRM, today" → use the iframe. If you need custom UX or deep CRM workflow integration (CTI, screen-pop) → use npm or UMD.

See also:

Cấp phép theo điều khoản sử dụng của Zorio.