WebRTC Phone API Doc

sachin
edited 6:34AM in SIP

WebRTC Phone JavaScript API

When the WebRTC Phone extension is installed, every http/https page gets a window.WebRTCPhone object that can place and control calls through the user's phone, for example to add call buttons to a CRM or help desk.

const call = await WebRTCPhone.call('5551234567');
// ...later
await WebRTCPhone.hangup(call.id);

Detecting the extension

window.WebRTCPhone is defined before your scripts run. Check for it, or listen for the webrtcphone:ready event if your code might run first:

if (window.WebRTCPhone) {
  showCallButtons();
} else {
  window.addEventListener('webrtcphone:ready', showCallButtons, { once: true });
}

Permission

The first time a site uses any method except getStatus(), the phone's side panel asks the user to Allow or Block that site. The answer is remembered; users can review and remove sites under Settings → Website API. A blocked site gets PERMISSION_DENIED without another prompt.

Call requestPermission() from a "Connect phone" button to ask up front instead of on the first call.

The phone's panel reflects every API action as if the user had clicked it there, and shows a short notice naming your site, e.g. "crm.example.com placed a call to 5551234567". Queries and DTMF tones don't show a notice. call() and answer() also switch the panel to its Dialer screen.

The phone must be open

Calls run in the extension's side panel. If it's closed:

  • call() and requestPermission() try to open it. Chrome only allows this when your code runs directly in a click handler; otherwise they fail with PHONE_NOT_OPEN.
  • Every other method fails with PHONE_NOT_OPEN.
  • getStatus() still works and reports phoneOpen: false.

Methods

Every method returns a Promise. Call IDs are strings, valid while the call exists.

MethodResolves withNotescall(number)CallPlaces a call. Waits up to 15 s if the phone is still connecting. number may be a phone number (formatting such as +, -, spaces and brackets is removed), an extension, or a SIP address (sip:alice@example.com).hangup(callId)—Ends a connected call, cancels an outgoing call that hasn't connected, or declines a ringing incoming call.answer(callId)—Ringing incoming calls only.reject(callId)—Ringing incoming calls only.hold(callId) / resume(callId)—Connected calls only.mute(callId) / unmute(callId)—Mutes the user's microphone on that call.sendDTMF(callId, digits)—Keypad tones on a connected call, e.g. '1234#' (1–32 of 0-9 * # A-D).transfer(callId, number)—Blind transfer of a connected call.sendMessage(number, text)—SIP text message, 1–1000 characters.getCalls()Call[]Current calls.getCall(callId)Call | nullgetStatus()StatusNever prompts; works while the phone is closed.requestPermission()'granted' | 'denied'

The phone handles one call at a time: call() fails with BUSY during a call, and an incoming call during a call is declined automatically.

Call

{
  id: string,
  number: string,          // e.g. "15551234567"
  remoteURI: string,       // e.g. "sip:15551234567@sip.provider.com"
  displayName: string,     // caller name if the network sent one, else ""
  direction: 'outgoing' | 'incoming',
  state: 'calling' | 'ringing' | 'connecting' | 'active' | 'held' | 'ended',
  held: boolean,
  muted: boolean,
  startTime: number,       // ms since epoch
  connectTime: number | null,
  endReason?: 'completed' | 'missed' | 'failed' | 'rejected' | 'cancelled'  // when state is 'ended'
}

Status

{
  phoneOpen: boolean,
  enabled: boolean,                       // Settings → Website API switch
  permission: 'granted' | 'denied' | 'prompt' | 'unknown',  // 'unknown' while the phone is closed
  registered?: boolean,                   // only when permission is 'granted'
  calls?: Call[]                          // only when permission is 'granted'
}

Events

WebRTCPhone is an EventTarget. Events carry their data in event.detail and are only delivered to sites the user allowed.

EventdetailWhenincomingcall{ call }A call starts ringing. Good for screen-pops.callstatechange{ call }Any call changes state, including when it ends.callended{ call }A call ended; call.endReason says how.registrationchange{ registered }The phone connected to or lost the SIP server.

WebRTCPhone.addEventListener('incomingcall', (e) => {
  openCustomerRecord(e.detail.call.number);
});
WebRTCPhone.addEventListener('callended', (e) => {
  logCall(e.detail.call.number, e.detail.call.endReason);
});

Events cover every call on the phone, not only calls your site placed.

Errors

Failures reject with an Error whose code says why:

codeMeaningPHONE_NOT_OPENThe side panel is closed (see above).PERMISSION_DENIEDThe user blocked this site.API_DISABLEDThe user turned the website API off in Settings.NOT_CONFIGUREDNo SIP account set up yet (Settings opens).NOT_REGISTEREDCouldn't connect to the SIP server within 15 s.BUSYAlready on a call.UNKNOWN_CALLNo current call with that ID.INVALID_STATEThe call isn't in a state that allows this (e.g. holding a ringing call).INVALID_ARGUMENTBad number, digits, text or method name.CALL_FAILEDThe call couldn't be placed.EXTENSION_UNAVAILABLEThe extension was updated; reload the page.FAILEDAnything else; see message.

try {
  await WebRTCPhone.call(number);
} catch (err) {
  if (err.code === 'PHONE_NOT_OPEN') alert('Open WebRTC Phone from the toolbar first.');
  else alert(err.message);
}

Try it

api-demo.html exercises every method and event. Serve it over http (content scripts don't run on file:// pages), e.g. from this folder: python -m http.server 8080, then open http://localhost:8080/api-demo.html.

Comments

  • Demo Code

    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="color-scheme" content="light dark" />
    <title>WebRTC Phone API Demo</title>
    <style>
      :root { --bg: #f4f5f7; --card: #fff; --text: #1b1f27; --muted: #656d7a; --border: #d8dce3; --accent: #2f6fed; --ok: #1f9d55; --err: #e0393e; }
      @media (prefers-color-scheme: dark) {
        :root { --bg: #14171c; --card: #1d2128; --text: #e7e9ee; --muted: #a2a9b5; --border: #2c323c; --accent: #5c8cff; }
      }
      * { box-sizing: border-box; }
      body { margin: 0; background: var(--bg); color: var(--text); font: 15px/1.5 -apple-system, "Segoe UI", Roboto, sans-serif; }
      main { max-width: 860px; margin: 0 auto; padding: 24px 16px 48px; display: grid; gap: 16px; }
      h1 { margin: 0; font-size: 1.5rem; }
      .card { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 16px; }
      .card h2 { margin: 0 0 12px; font-size: 1rem; }
      .row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 8px; }
      input { padding: 8px 10px; border-radius: 8px; border: 1px solid var(--border); background: var(--bg); color: var(--text); font: inherit; min-width: 0; flex: 1 1 160px; }
      button { padding: 8px 14px; border-radius: 8px; border: 1px solid var(--border); background: var(--bg); color: var(--text); font: inherit; cursor: pointer; }
      button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
      #status { font-weight: 600; }
      #log { margin: 0; max-height: 320px; overflow: auto; font: 12.5px/1.5 ui-monospace, Consolas, monospace; white-space: pre-wrap; word-break: break-word; }
      .ok { color: var(--ok); } .err { color: var(--err); } .evt { color: var(--accent); }
    </style>
    </head>
    <body>
    <main>
      <h1>WebRTC Phone API demo</h1>
      <div class="card">
        <h2>Status: <span id="status">checking…</span></h2>
        <div class="row">
          <button id="btn-status">getStatus()</button>
          <button id="btn-permission">requestPermission()</button>
          <button id="btn-calls">getCalls()</button>
        </div>
      </div>
    
    
      <div class="card">
        <h2>Call</h2>
        <div class="row">
          <input id="number" placeholder="Number, e.g. 5551234567" />
          <button class="primary" id="btn-call">call()</button>
        </div>
        <div class="row">
          <input id="call-id" placeholder="Call ID (filled in automatically)" />
        </div>
        <div class="row">
          <button data-method="hangup">hangup()</button>
          <button data-method="answer">answer()</button>
          <button data-method="reject">reject()</button>
          <button data-method="hold">hold()</button>
          <button data-method="resume">resume()</button>
          <button data-method="mute">mute()</button>
          <button data-method="unmute">unmute()</button>
        </div>
        <div class="row">
          <input id="digits" placeholder="DTMF digits, e.g. 123#" />
          <button id="btn-dtmf">sendDTMF()</button>
        </div>
        <div class="row">
          <input id="transfer-to" placeholder="Transfer to number" />
          <button id="btn-transfer">transfer()</button>
        </div>
      </div>
    
    
      <div class="card">
        <h2>Message</h2>
        <div class="row">
          <input id="msg-to" placeholder="To number" />
          <input id="msg-text" placeholder="Text" />
          <button id="btn-message">sendMessage()</button>
        </div>
      </div>
    
    
      <div class="card">
        <h2>Log</h2>
        <pre id="log"></pre>
      </div>
    </main>
    
    
    <script>
      const $ = (id) => document.getElementById(id);
      const logEl = $('log');
    
    
      function log(text, cls) {
        const line = document.createElement('div');
        line.className = cls || '';
        line.textContent = `${new Date().toLocaleTimeString()}  ${text}`;
        logEl.prepend(line);
      }
    
    
      async function run(label, fn) {
        try {
          const result = await fn();
          log(`${label} → ${result === undefined ? 'done' : JSON.stringify(result)}`, 'ok');
          return result;
        } catch (err) {
          log(`${label} ✗ ${err.code || ''} ${err.message}`, 'err');
          return undefined;
        }
      }
    
    
      function start() {
        const phone = window.WebRTCPhone;
        $('status').textContent = `extension found (API v${phone.version})`;
    
    
        ['incomingcall', 'callstatechange', 'callended', 'registrationchange'].forEach((type) => {
          phone.addEventListener(type, (e) => {
            log(`event ${type}: ${JSON.stringify(e.detail)}`, 'evt');
            if (e.detail.call && e.detail.call.state !== 'ended') $('call-id').value = e.detail.call.id;
          });
        });
    
    
        $('btn-status').onclick = () => run('getStatus()', () => phone.getStatus());
        $('btn-permission').onclick = () => run('requestPermission()', () => phone.requestPermission());
        $('btn-calls').onclick = () => run('getCalls()', () => phone.getCalls());
        $('btn-call').onclick = async () => {
          const call = await run(`call(${$('number').value})`, () => phone.call($('number').value));
          if (call) $('call-id').value = call.id;
        };
        document.querySelectorAll('[data-method]').forEach((btn) => {
          btn.onclick = () => run(`${btn.dataset.method}(${$('call-id').value})`, () => phone[btn.dataset.method]($('call-id').value));
        });
        $('btn-dtmf').onclick = () => run('sendDTMF()', () => phone.sendDTMF($('call-id').value, $('digits').value));
        $('btn-transfer').onclick = () => run('transfer()', () => phone.transfer($('call-id').value, $('transfer-to').value));
        $('btn-message').onclick = () => run('sendMessage()', () => phone.sendMessage($('msg-to').value, $('msg-text').value));
    
    
        run('getStatus()', () => phone.getStatus());
      }
    
    
      if (window.WebRTCPhone) start();
      else {
        $('status').textContent = 'extension not detected';
        window.addEventListener('webrtcphone:ready', start, { once: true });
      }
    </script>
    </body>
    </html>
    
    
    
Sign In or Register to comment.

Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!