Quickstart HTML
File HTML đầy đủ copy-paste chạy ngay — không cần build pipeline, không cần npm install. Mở bằng browser (qua HTTPS hoặc localhost) là gọi điện được.
Yêu cầu trước khi chạy
- Trang phải serve qua HTTPS (WebRTC mandate). Test local có thể dùng
localhostthường. - Browser ≥ Chrome 90 / Edge 90 / Firefox 90 / Safari 14.
- Có Bearer token lấy từ
POST /api/auth/logincủa tài khoản Zorio đang dùng. - Đã cấp quyền microphone cho trang.
1. Quickstart tối giản (~20 dòng)
<!DOCTYPE html>
<html lang="vi">
<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 sẵn sàng — gõ phone.call("0912345678") để gọi thử')
})
</script>
</body>
</html>2. Quickstart đầy đủ — dialer + status + log + 12 events
File HTML standalone dưới đây bao gồm: form login token, dialer pad, status badge, real-time event log, error banner. Đây là template Zorio dùng để demo cho đối tác.
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8" />
<title>Zorio Webphone — Quickstart đầy đủ</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>Gọi</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 lần ${p.attempt}…`, 'warn') })
phone.on('audio_autoplay_blocked', () => {
log('audio_autoplay_blocked')
setStatus('Hãy click vào trang để bật âm thanh', '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>3. Cách dùng
- Copy toàn bộ HTML phía trên vào file
quickstart.html. - Serve qua HTTPS (vd
npx http-server -S -C cert.pem -K key.pem), hoặc mở qualocalhost. - Mở browser, nhập:
apiBase: URL truy cập của khách hàng (vdhttps://acme.zorio.example).token: Bearer token lấy từPOST /api/auth/login.
- Bấm Connect → đợi status hiện "Online" → nhập số → bấm Gọi.
4. Quan sát feature v7 trong log
Khi mất mạng tạm thời, log sẽ hiện chuỗi:
10:14:32 disconnected {"reason":"transport_disconnect"}
10:14:33 auto_reconnect {"attempt":1,"delay_ms":1000}
10:14:34 connected
10:14:34 registeredCuộc gọi outbound qua di động sẽ thấy call_ringing (có audio ringback) trước khi call_answered — nhờ early media.
Mở rộng
Thêm cuộc gọi outbound trong campaign telesales:
const call = await phone.call('0912345678', {
campaignId: 36,
leadId: 78912,
recording: true,
meta: { script_id: 12, agent_note: 'gọi lại sau 14h' },
})campaignId / leadId sẽ link CDR với telesales campaign, hiện ngay trong Reports + Live monitoring của khách hàng.
Token expose trong quickstart
File này dán bearer token thẳng vào HTML → CHỈ dùng cho demo nội bộ. Production luôn fetch token qua endpoint backend của đối tác, kèm tokenResolver để auto-refresh khi 401:
ZorioWebphone.connect({
apiBase,
token: currentToken,
tokenResolver: async () => {
const r = await fetch('/my-app/refresh-zorio-token', { method: 'POST' })
return (await r.json()).token
},
})Tham khảo thêm:
- Nhúng qua npm — production-grade
- API methods — đầy đủ signature TypeScript
- Events — payload từng event
