Skip to content

Quickstart HTML

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

  • The page must be served over HTTPS (WebRTC mandate). For local testing, plain localhost is fine.
  • Browser >= Chrome 90 / Edge 90 / Firefox 90 / Safari 14.
  • A bearer token obtained from POST /api/auth/login for the Zorio account in use.
  • Microphone permission granted to the page.

1. Minimal quickstart (~20 lines)

html
<!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>

2. Full quickstart — dialer + status + log + 12 events

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.

html
<!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>

3. How to use

  1. Copy the HTML above into a file quickstart.html.
  2. Serve over HTTPS (e.g. npx http-server -S -C cert.pem -K key.pem), or open it via localhost.
  3. Open the browser and enter:
    • apiBase: the customer's URL (e.g. https://acme.zorio.example).
    • token: the bearer token from POST /api/auth/login.
  4. Click Connect → wait for status to show "Online" → enter a number → click Call.

4. Observing v7 features in the log

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  registered

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

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

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

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