English
English
Appearance
English
English
Appearance
The Webphone SDK emits 12 events covering the SIP registration lifecycle + call lifecycle + system errors. Subscribe via phone.on(eventName, handler) and unsubscribe via phone.off(eventName, handler?).
Handler scope
Handlers are called synchronously when the event fires. If you need heavy async work inside a handler (API calls, DB writes), await separately so you do not block the SDK event loop.
| # | Event | Payload | When emitted |
|---|---|---|---|
| 1 | registered | {} | SIP register succeeded for the first time, or after a reconnect |
| 2 | unregistered | { reason } | SIP unregister (manual / remote / transport_disconnect) |
| 3 | connected | {} | WebSocket WSS is up — not yet registered |
| 4 | disconnected | { reason? } | WebSocket dropped, before the auto-reconnect kicks in |
| 5 | incoming_call | { call, contact } | An INVITE arrived |
| 6 | call_ringing | { call } | Outbound received 180/183, or inbound is locally ringing |
| 7 | call_answered | { call } | The call entered answered state |
| 8 | call_hangup | { call, reason, billsec } | Either side hung up, or the call failed |
| 9 | dtmf | { call, digit } | DTMF received from the remote party (RFC 2833) |
| 10 | error | ZorioWebphoneError | Any runtime error — must be handled |
| 11 | auto_reconnect | { attempt, delay_ms } | Each time the supervisor schedules a reconnect attempt |
| 12 | audio_autoplay_blocked | { call } | The browser blocked autoplay when a call arrived |
registered phone.on('registered', () => {
console.log('SIP ready, extension:', phone.extension)
showBanner('Online', 'success')
})Emitted on the first registration (after connect()) and on every successful re-register after a reconnect. Use it to surface online status in the UI.
unregistered phone.on('unregistered', ({ reason }) => {
// reason: 'manual' | 'remote' | 'transport_disconnect'
if (reason === 'transport_disconnect') {
showBanner('Connection lost, reconnecting…', 'warn')
}
})reason | Meaning |
|---|---|
manual | The user explicitly called phone.unregister() |
remote | The Zorio PBX returned 401/403, possibly due to a credential change |
transport_disconnect | The WebSocket WSS dropped |
connected phone.on('connected', () => {
console.log('WSS connected — about to register')
})The WebSocket layer is up but the SIP REGISTER has not completed yet. Useful for showing a 2-step progress: "Connecting…" → "Registering…" → "Ready".
disconnected phone.on('disconnected', ({ reason }) => {
console.warn('WSS drop:', reason)
})The WebSocket closed. The SDK supervisor will retry — followed by auto_reconnect.
incoming_call phone.on('incoming_call', ({ call, contact }) => {
myUI.showIncomingPopup({
name: contact?.full_name || call.number,
onAccept: () => call.answer(),
onReject: () => call.hangup(),
})
})Full payload:
{
call: ZorioCall, // direction='incoming', state='ringing'
contact: {
id: number
full_name: string
phones?: string[]
customer_id?: number
lead_id?: number
} | null
}contact is looked up from the Zorio backend (/api/contacts/lookup?number=...). If the customer is not yet in CSKH → contact = null, display the raw call.number.
call_ringing phone.on('call_ringing', ({ call }) => {
if (call.direction === 'outgoing') {
showStatus(`Calling ${call.number}…`)
}
})Early media (v7)
For outbound, this event fires when the SDK receives a SIP 180 Ringing or 183 Session Progress. Thanks to earlyMedia: true, you hear ringback / IVR carrier audio right at this point — no need to wait for an answer.
call_answered phone.on('call_answered', ({ call }) => {
console.log('Connected, UUID:', call.uuid)
startTimer(call.uuid)
})The remote side answered (200 OK + ACK). Start billing/billsec from this moment.
call_hangup phone.on('call_hangup', ({ call, reason, billsec }) => {
stopTimer(call.uuid)
if (reason === 'no_answer') {
showToast('No answer')
} else if (reason === 'busy') {
showToast('Busy')
}
saveBillsec(call.uuid, billsec)
})Common reason | Source Zorio PBX HangupCause |
|---|---|
normal_clearing | NORMAL_CLEARING (billsec > 0) |
no_answer | NORMAL_CLEARING + billsec = 0 or NO_ANSWER |
busy | USER_BUSY |
unallocated_number | UNALLOCATED_NUMBER |
originator_cancel | ORIGINATOR_CANCEL |
call_rejected | CALL_REJECTED |
For the complete mapping see Hangup cause codes.
dtmf phone.on('dtmf', ({ call, digit }) => {
console.log('Remote pressed:', digit)
})A DTMF was received from the remote party (e.g. the customer pressing a key in a reverse IVR). digit is a single character 0-9 * # A-D.
error phone.on('error', (err) => {
switch (err.code) {
case 'bootstrap_failed':
alert('Could not connect to Zorio — check the token / network')
break
case 'audio_autoplay_blocked':
showBanner('Please click the page to enable audio')
break
case 'reconnect_failed':
showBanner('Connection lost for too long — please reload the page')
break
default:
console.error('[zorio-webphone]', err.code, err.message, err.details)
}
})A catch-all for every runtime error. The ZorioWebphoneError class:
class ZorioWebphoneError extends Error {
code: string // machine-readable
details?: Record<string, unknown>
}See the full list of code values at API methods → Error handling.
auto_reconnect phone.on('auto_reconnect', ({ attempt, delay_ms }) => {
console.log(`Reconnect attempt ${attempt}, waiting ${delay_ms}ms`)
if (attempt > 5) showBanner('Connection unstable', 'warn')
})Emitted every time the v7 supervisor schedules a reconnect. Exponential backoff: 1s → 2s → 4s → 8s → 16s → 30s (cap) + 0-500ms of jitter. Resets after a successful re-register.
Recommended UX
Show a discreet "Reconnecting…" banner only when attempt >= 2. Do not panic the user on the first attempt — a 1-2s network blip is normal.
audio_autoplay_blocked phone.on('audio_autoplay_blocked', ({ call }) => {
// Safari/iOS blocks autoplay when the tab has no user gesture yet
myUI.showUnlockBanner({
message: 'Tap here to enable call audio',
onClick: async () => {
await phone.unlockAudio() // creates a gesture context for the audio element
},
})
})Safari / iOS quirk
The browser blocks audio.play() until the page has had a user interaction (click/tap). The SDK detects this → emits this event → the host app MUST display a button asking the user to tap. If you ignore it, the call still connects but the user cannot hear audio.
const phone = await ZorioWebphone.connect({ apiBase, token, mount: '#zorio-phone' })
phone.on('incoming_call', ({ call, contact }) => myUI.showIncoming(call, contact))
phone.on('call_answered', ({ call }) => myUI.openInCall(call))
phone.on('call_hangup', ({ call, billsec }) => myUI.closeInCall(call, billsec))
phone.on('error', (err) => console.error(err))function updateStatusBadge() {
badge.textContent = phone.status === 'registered' ? 'Online' : 'Offline'
}
phone.on('registered', updateStatusBadge)
phone.on('unregistered', updateStatusBadge)
phone.on('connected', updateStatusBadge)
phone.on('disconnected', updateStatusBadge)
phone.on('auto_reconnect', updateStatusBadge)See also: