English
English
Appearance
English
English
Appearance
A complete HTML file you can copy-paste and run immediately — no build pipeline, no npm install. Open it in a browser (over HTTPS or localhost) and you can place a call.
Prerequisites
localhost is fine.POST /api/auth/login for the Zorio account in use.<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Zorio Webphone Quickstart</title>
</head>
<body>
<div id="zorio-phone" style="position:fixed;bottom:24px;right:24px"></div>
<script src="https://cdn.zorio.vn/webphone-sdk/v1/zorio-webphone.umd.cjs"></script>
<script>
const { ZorioWebphone } = window.ZorioWebphoneSDK
ZorioWebphone.connect({
apiBase: 'https://acme.zorio.example',
token: 'PASTE-BEARER-TOKEN-HERE',
mount: '#zorio-phone',
}).then((phone) => {
window.phone = phone
console.log('Webphone ready — type phone.call("0912345678") to test')
})
</script>
</body>
</html>The standalone HTML below includes: a token-login form, dialer pad, status badge, real-time event log, and an error banner. It is the template Zorio uses to demo for partners.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Zorio Webphone — Full Quickstart</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 880px; margin: 24px auto; padding: 0 16px; }
.row { display: flex; gap: 12px; align-items: center; margin: 12px 0; }
input, button { padding: 8px 12px; font-size: 14px; border-radius: 6px; border: 1px solid #cbd5e1; }
button { cursor: pointer; background: #2563eb; color: #fff; border-color: #2563eb; }
button.secondary { background: #f1f5f9; color: #0f172a; border-color: #cbd5e1; }
button.danger { background: #dc2626; border-color: #dc2626; }
button:disabled { opacity: 0.5; cursor: not-allowed; }
#status { font-weight: 600; padding: 4px 10px; border-radius: 999px; background: #f1f5f9; }
#status.ready { background: #d1fae5; color: #065f46; }
#status.error { background: #fee2e2; color: #991b1b; }
#status.warn { background: #fef3c7; color: #92400e; }
#log { background: #0f172a; color: #e2e8f0; font-family: ui-monospace, monospace;
font-size: 12px; padding: 12px; border-radius: 8px; height: 280px;
overflow-y: auto; white-space: pre-wrap; }
.log-row { margin: 2px 0; }
.log-row .ts { color: #94a3b8; }
.log-row .ev { color: #38bdf8; font-weight: 600; }
#zorio-phone { position: fixed; bottom: 24px; right: 24px; }
</style>
</head>
<body>
<h1>Zorio Webphone — Quickstart</h1>
<div class="row">
<input id="api-base" placeholder="https://acme.zorio.example" style="flex:1" />
<input id="token" placeholder="Bearer token" style="flex:2" />
<button id="btn-connect">Connect</button>
<button id="btn-disconnect" class="secondary" disabled>Disconnect</button>
<span id="status">Idle</span>
</div>
<div class="row">
<input id="dial" placeholder="0912345678" style="flex:1" />
<button id="btn-call" disabled>Call</button>
<button id="btn-hangup" class="danger" disabled>Hangup</button>
<button id="btn-answer" class="secondary" disabled>Answer</button>
<button id="btn-mute" class="secondary" disabled>Mute</button>
<button id="btn-hold" class="secondary" disabled>Hold</button>
</div>
<div id="log"></div>
<div id="zorio-phone"></div>
<script src="https://cdn.zorio.vn/webphone-sdk/v1/zorio-webphone.umd.cjs"></script>
<script>
const { ZorioWebphone } = window.ZorioWebphoneSDK
const $ = (id) => document.getElementById(id)
let phone = null
let activeCall = null
function setStatus(text, cls) {
const el = $('status')
el.textContent = text
el.className = cls || ''
}
function log(event, payload) {
const ts = new Date().toISOString().slice(11, 19)
const row = document.createElement('div')
row.className = 'log-row'
row.innerHTML =
`<span class="ts">${ts}</span> ` +
`<span class="ev">${event}</span> ` +
(payload !== undefined ? JSON.stringify(payload) : '')
$('log').appendChild(row)
$('log').scrollTop = $('log').scrollHeight
}
function setActiveCall(call) {
activeCall = call
$('btn-hangup').disabled = !call
$('btn-answer').disabled = !(call && call.direction === 'incoming' && call.state === 'ringing')
$('btn-mute').disabled = !(call && call.state === 'answered')
$('btn-hold').disabled = !(call && call.state === 'answered')
$('btn-mute').textContent = call?.muted ? 'Unmute' : 'Mute'
$('btn-hold').textContent = call?.onHold ? 'Unhold' : 'Hold'
}
$('btn-connect').onclick = async () => {
try {
setStatus('Connecting…', 'warn')
phone = await ZorioWebphone.connect({
apiBase: $('api-base').value.trim(),
token: $('token').value.trim(),
mount: '#zorio-phone',
debug: true,
})
window.phone = phone
// === 12 events ===
phone.on('registered', () => { log('registered'); setStatus('Online', 'ready') })
phone.on('unregistered', (p) => { log('unregistered', p); setStatus('Offline', 'warn') })
phone.on('connected', () => log('connected'))
phone.on('disconnected', (p) => { log('disconnected', p); setStatus('Disconnected', 'warn') })
phone.on('incoming_call', ({ call, contact }) => {
log('incoming_call', { number: call.number, contact: contact?.full_name })
setActiveCall(call)
})
phone.on('call_ringing', ({ call }) => { log('call_ringing', { number: call.number }); setActiveCall(call) })
phone.on('call_answered', ({ call }) => { log('call_answered', { uuid: call.uuid }); setActiveCall(call) })
phone.on('call_hangup', ({ call, reason, billsec }) => {
log('call_hangup', { reason, billsec })
setActiveCall(null)
})
phone.on('dtmf', ({ digit }) => log('dtmf', { digit }))
phone.on('error', (err) => { log('error', { code: err.code, msg: err.message }); setStatus('Error: ' + err.code, 'error') })
phone.on('auto_reconnect', (p) => { log('auto_reconnect', p); setStatus(`Reconnect attempt ${p.attempt}…`, 'warn') })
phone.on('audio_autoplay_blocked', () => {
log('audio_autoplay_blocked')
setStatus('Click the page to enable audio', 'warn')
})
$('btn-connect').disabled = true
$('btn-disconnect').disabled = false
$('btn-call').disabled = false
} catch (err) {
log('connect-failed', { code: err.code, msg: err.message })
setStatus('Connect failed', 'error')
}
}
$('btn-disconnect').onclick = async () => {
await phone?.disconnect()
phone = null
setActiveCall(null)
setStatus('Idle', '')
$('btn-connect').disabled = false
$('btn-disconnect').disabled = true
$('btn-call').disabled = true
}
$('btn-call').onclick = async () => {
const num = $('dial').value.trim()
if (!num) return
try {
const call = await phone.call(num, { meta: { source: 'quickstart' } })
setActiveCall(call)
} catch (err) {
log('call-failed', { code: err.code, msg: err.message })
}
}
$('btn-hangup').onclick = () => activeCall?.hangup()
$('btn-answer').onclick = () => activeCall?.answer()
$('btn-mute').onclick = async () => {
if (!activeCall) return
if (activeCall.muted) await activeCall.unmute()
else await activeCall.mute()
setActiveCall(activeCall)
}
$('btn-hold').onclick = async () => {
if (!activeCall) return
if (activeCall.onHold) await activeCall.unhold()
else await activeCall.hold()
setActiveCall(activeCall)
}
</script>
</body>
</html>quickstart.html.npx http-server -S -C cert.pem -K key.pem), or open it via localhost.apiBase: the customer's URL (e.g. https://acme.zorio.example).token: the bearer token from POST /api/auth/login.When the network drops momentarily, the log shows:
10:14:32 disconnected {"reason":"transport_disconnect"}
10:14:33 auto_reconnect {"attempt":1,"delay_ms":1000}
10:14:34 connected
10:14:34 registeredOutbound calls to mobile networks show call_ringing (with audible ringback) before call_answered — thanks to early media.
Extension
Place an outbound call inside a telesales campaign:
const call = await phone.call('0912345678', {
campaignId: 36,
leadId: 78912,
recording: true,
meta: { script_id: 12, agent_note: 'call back after 2pm' },
})campaignId / leadId link the CDR to the telesales campaign, immediately visible in the customer's Reports and Live monitoring views.
Token exposure in the quickstart
This file pastes the bearer token straight into the HTML — for internal demos ONLY. In production, always fetch the token via the partner's backend endpoint and supply tokenResolver to auto-refresh on 401:
ZorioWebphone.connect({
apiBase,
token: currentToken,
tokenResolver: async () => {
const r = await fetch('/my-app/refresh-zorio-token', { method: 'POST' })
return (await r.json()).token
},
})See also: