English
English
Appearance
English
English
Appearance
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.
<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.
<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:
{
"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.
[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).
<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>| Parameter | Value | Description |
|---|---|---|
token | embed JWT | Required — short-lived embed token |
layout | docked | overlay | fullscreen | UI layout. Default: docked |
theme | light | dark | auto | Default: auto |
locale | vi | en | Default: vi |
auto_register | 1 | 0 | Auto-register SIP after load. Default: 1 |
You MUST add allow="microphone" — without it the browser will block getUserMedia even when the user has already granted permission to the parent page:
<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.
The webphone iframe communicates with the parent via window.postMessage. Every message uses the same shape:
interface ZorioWebphoneMessage {
source: 'zorio-webphone' // marker, ignore other messages
type: string // event name or command
payload?: any // accompanying data
}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.
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.
<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>The iframe is built from the same SDK bundle, so it carries everything:
| Criterion | iframe | SDK npm/UMD |
|---|---|---|
| Integration speed | Fastest (one <iframe> tag) | Medium |
| Custom UI | Stuck with the default Zorio UI | Full control |
| Cross-origin | Fully isolated | Requires CSP setup |
| Advanced event customization | postMessage | Native callbacks |
| Air-gap | Self-host the iframe internally | Self-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: