Skip to content

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 localhost thường.
  • Browser ≥ Chrome 90 / Edge 90 / Firefox 90 / Safari 14.
  • Bearer token lấy từ POST /api/auth/login củ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)

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

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

  1. Copy toàn bộ HTML phía trên vào file quickstart.html.
  2. Serve qua HTTPS (vd npx http-server -S -C cert.pem -K key.pem), hoặc mở qua localhost.
  3. Mở browser, nhập:
    • apiBase: URL truy cập của khách hàng (vd https://acme.zorio.example).
    • token: Bearer token lấy từ POST /api/auth/login.
  4. 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  registered

Cuộ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:

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

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

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