Skip to content

Events

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.

Summary table

#EventPayloadWhen emitted
1registered{}SIP register succeeded for the first time, or after a reconnect
2unregistered{ reason }SIP unregister (manual / remote / transport_disconnect)
3connected{}WebSocket WSS is up — not yet registered
4disconnected{ reason? }WebSocket dropped, before the auto-reconnect kicks in
5incoming_call{ call, contact }An INVITE arrived
6call_ringing{ call }Outbound received 180/183, or inbound is locally ringing
7call_answered{ call }The call entered answered state
8call_hangup{ call, reason, billsec }Either side hung up, or the call failed
9dtmf{ call, digit }DTMF received from the remote party (RFC 2833)
10errorZorioWebphoneErrorAny runtime error — must be handled
11auto_reconnect{ attempt, delay_ms }Each time the supervisor schedules a reconnect attempt
12audio_autoplay_blocked{ call }The browser blocked autoplay when a call arrived

1. registered

ts
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.

2. unregistered

ts
phone.on('unregistered', ({ reason }) => {
  // reason: 'manual' | 'remote' | 'transport_disconnect'
  if (reason === 'transport_disconnect') {
    showBanner('Connection lost, reconnecting…', 'warn')
  }
})
reasonMeaning
manualThe user explicitly called phone.unregister()
remoteThe Zorio PBX returned 401/403, possibly due to a credential change
transport_disconnectThe WebSocket WSS dropped

3. connected

ts
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".

4. disconnected

ts
phone.on('disconnected', ({ reason }) => {
  console.warn('WSS drop:', reason)
})

The WebSocket closed. The SDK supervisor will retry — followed by auto_reconnect.

5. incoming_call

ts
phone.on('incoming_call', ({ call, contact }) => {
  myUI.showIncomingPopup({
    name: contact?.full_name || call.number,
    onAccept: () => call.answer(),
    onReject: () => call.hangup(),
  })
})

Full payload:

ts
{
  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.

6. call_ringing

ts
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.

7. call_answered

ts
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.

8. call_hangup

ts
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 reasonSource Zorio PBX HangupCause
normal_clearingNORMAL_CLEARING (billsec > 0)
no_answerNORMAL_CLEARING + billsec = 0 or NO_ANSWER
busyUSER_BUSY
unallocated_numberUNALLOCATED_NUMBER
originator_cancelORIGINATOR_CANCEL
call_rejectedCALL_REJECTED

For the complete mapping see Hangup cause codes.

9. dtmf

ts
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.

10. error

ts
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:

ts
class ZorioWebphoneError extends Error {
  code:     string                            // machine-readable
  details?: Record<string, unknown>
}

See the full list of code values at API methods → Error handling.

11. auto_reconnect

ts
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.

12. audio_autoplay_blocked

ts
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.

Common patterns

Wire the 3 minimum events for an MVP

ts
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))

Track online state for a dashboard

ts
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:

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