WebRTC Phone API Doc
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()andrequestPermission()try to open it. Chrome only allows this when your code runs directly in a click handler; otherwise they fail withPHONE_NOT_OPEN.- Every other method fails with
PHONE_NOT_OPEN. getStatus()still works and reportsphoneOpen: 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>0
Howdy, Stranger!
Categories
- 103 All Categories
- 27 VoIP
- 14 SIP
- 16 asterisk
- 44 Programming
- 1 Nodejs
- 4 javascript
- 19 PHP
- 8 Codeigniter
- 14 database
- 1 UI/UX
- 2 Flutter
- 29 OS
- 27 Linux
- 1 Virtualization
- 1 Android
- 1 Windows
- 3 legal
