npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@datavoice/webphone-sdk

v0.4.6

Published

Framework-agnostic TypeScript SDK for integrating DataVoice WebPhone, SIP/WebRTC calling, and real-time PBX events through SignalR.

Readme

@datavoice/webphone-sdk

Enterprise-grade TypeScript SDK for SIP/WebRTC telephony and Asterisk PBX integration in the browser.

Provides SIP registration, bidirectional audio calls, hold/mute/transfer/DTMF/conference, real-time PBX event streaming via the DataVoice Dispatcher hub, and call history — behind a typed, event-driven API with no jQuery or framework dependencies.


Requirements

| | Minimum | |---|---| | Browser | Chrome 80+ / Edge 80+ / Firefox 78+ | | Node.js (build) | 18.0.0+ | | Page context | HTTPS or localhost — required for getUserMedia and WSS | | SIP peer dep | sip.js ^0.21.0 | | Dispatcher peer dep | @microsoft/signalr ^8.0.0 (optional) |


Installation

npm install @datavoice/webphone-sdk sip.js@^0.21.0

# Only if using PBX Dispatcher event streaming:
npm install @microsoft/signalr@^8.0.0

REST-only usage (DataVoicePlatform — Users, DialPlan, Telephony), no SIP calling: import from the /platform subpath instead of the package root. It has zero peer dependencies — neither sip.js nor @microsoft/signalr is ever resolved:

npm install @datavoice/webphone-sdk
import { DataVoicePlatform } from '@datavoice/webphone-sdk/platform';

Importing DataVoicePlatform from the package root also works and is fully backward compatible, but the root entry bundles DataVoiceWebPhone alongside it, which does require sip.js/@microsoft/signalr to be installed even if you never use them. Use the root entry only when you also need SIP calling in the same app.


Quick Start

import { DataVoiceWebPhone } from '@datavoice/webphone-sdk';

const phone = new DataVoiceWebPhone({
  sip: {
    extension:   '3402',
    password:    'your-sip-password',   // never log or persist
    realm:       'pbx.datavoice.com.mx',
    wsServer:    'wss://pbx.datavoice.com.mx:8089/ws',
    displayName: 'Agent Name',
  },
  media: {
    iceServers: [{ urls: 'stun:ns3.datavoice.com.mx:3478' }],
  },
  // Optional: PBX event streaming
  dispatcher: {
    url: 'https://dvlite.datavoice.com.mx:2443',
  },
  logging: { level: 'warn' },
});

// Attach remote audio to an <audio> element
//
// Every event handler receives the full { type, timestamp, payload } envelope,
// not the payload flattened onto the handler argument — destructure `payload`
// first, then read fields off it.
phone.on('media:remote:stream', ({ payload: { stream } }) => {
  const audio = document.getElementById('remoteAudio') as HTMLAudioElement;
  audio.srcObject = stream;
  audio.play().catch(console.error);
});

// Connect (register SIP + connect Dispatcher)
await phone.connect();

// Make a call
const call = await phone.call('7000');

// Handle incoming calls
phone.on('call:incoming', async ({ payload: { call } }) => {
  await phone.answer(call.id);
});

// Hang up
await phone.hangup(call.id);

// Clean disconnect
await phone.disconnect();

Required HTML element:

<audio id="remoteAudio" autoplay></audio>

Configuration

sip (required)

| Field | Type | Default | Description | |-------|------|---------|-------------| | extension | string | — | SIP extension number | | password | string | — | SIP auth password. Never log or persist. | | realm | string | — | SIP domain, e.g. "pbx.datavoice.com.mx" | | wsServer | string | — | WebSocket URL: "wss://pbx.datavoice.com.mx:8089/ws" | | displayName | string | extension | Caller ID name | | registerExpires | number | 600 | REGISTER expiry in seconds | | traceSip | boolean | false | Log raw SIP messages (debug only) | | logLevel | 0–3 | 0 | SIP.js verbosity (0=errors, 3=debug) | | routeSet | string | — | Route header for WebRTC-to-SIP gateway proxies |

media (optional)

| Field | Type | Default | Description | |-------|------|---------|-------------| | audio | boolean | true | Request microphone | | video | boolean | false | Request camera | | iceServers | RTCIceServer[] | DataVoice STUN | STUN/TURN servers | | iceCheckingTimeout | number | 1000 | ICE gathering timeout in ms |

dispatcher (optional — PBX event streaming)

| Field | Type | Default | Description | |-------|------|---------|-------------| | url | string | — | Dispatcher hub URL | | accessToken | string | — | JWT bearer token (if hub requires auth) | | staleStateThresholdMs | number | 30000 | ms before disconnected state is considered stale |

signalR (optional — legacy command hub)

For the legacy ASP.NET SignalR 1.x hub (agentConnectHub). Requires jquery.signalR-2.4.0 loaded via CDN and the four workspace context IDs (IdEspacioTrabajo, IdEquipoTrabajo, IdUsuario, IdTipoUsuario) from the DataVoice backend.

storage (optional)

| Field | Type | Default | Description | |-------|------|---------|-------------| | callHistory | CallHistoryProvider | LocalStorageCallHistoryProvider | Swap the storage backend | | localStorageKey | string | "sipCalls" | localStorage key | | maxHistoryEntries | number | 20 | Maximum retained history entries |

Available providers: LocalStorageCallHistoryProvider, IndexedDBCallHistoryProvider, InMemoryCallHistoryProvider, NullCallHistoryProvider.

ui (optional)

| Field | Type | Default | Description | |-------|------|---------|-------------| | answerMode | 'ring' \| 'auto-answer' \| 'do-not-disturb' | 'ring' | Initial answer mode | | maxConcurrentCalls | number | 0 (unlimited) | Max simultaneous calls |


Events

Subscribe with phone.on(event, handler) or wildcard phone.onPattern('call:*', handler).

Every handler receives the full { type, timestamp, payload } envelope — not the payload flattened onto the handler argument. Destructure payload (or nested-destructure straight through it, as below) to get at the event's actual fields.

Registration & Transport

phone.on('registration:registered',   ({ payload: { extension } }) => {});
phone.on('registration:unregistered', () => {});
phone.on('registration:failed',       ({ payload: { cause } }) => {});

phone.on('system:transport:sip:connected',    () => {});
phone.on('system:transport:sip:disconnected', ({ payload: { error } }) => {});

Call Lifecycle

phone.on('call:incoming',  ({ payload: { call } }) => {});           // incoming INVITE
phone.on('call:created',   ({ payload: { call } }) => {});           // outgoing INVITE sent
phone.on('call:ringing',   ({ payload: { call } }) => {});           // 180 Ringing
phone.on('call:answered',  ({ payload: { call } }) => {});           // session established
phone.on('call:connected', ({ payload: { call, hasVideo } }) => {}); // media flowing
phone.on('call:held',      ({ payload: { call } }) => {});
phone.on('call:resumed',   ({ payload: { call } }) => {});
phone.on('call:completed', ({ payload: { call, historyEntry } }) => {}); // BYE / ended
phone.on('call:failed',    ({ payload: { call, cause } }) => {});

phone.on('call:dtmf:sent',     ({ payload: { callId, digit, method } }) => {}); // confirmed; method: 'rfc4733' | 'info'
phone.on('call:dtmf:failed',   ({ payload: { callId, digit, statusCode } }) => {}); // far end rejected (SIP INFO path)
phone.on('call:dtmf:received', ({ payload: { callId, digit } }) => {});

phone.on('call:transfer:blind:started',   ({ payload: { call, destination } }) => {});
phone.on('call:transfer:blind:completed', ({ payload: { callId, destination } }) => {});
phone.on('call:transfer:blind:failed',    ({ payload: { call, cause } }) => {});

Media

phone.on('media:remote:stream', ({ payload: { callId, stream, hasVideo } }) => {
  audioEl.srcObject = stream;   // always wire this
});

PBX Events (Dispatcher hub)

phone.on('pbx:agent:connected', ({ payload: { agentExtension, queue, callerIdNum, callGlobalId, pbxId } }) => {});
phone.on('pbx:agent:completed', ({ payload: { agentExtension, queue, talkTimeMs, callGlobalId } }) => {});
phone.on('pbx:call:hangup',     ({ payload: { channel, cause, causeText, callGlobalId } }) => {});
phone.on('pbx:event:unknown',   ({ payload: { eventType, rawPayload } }) => {});

phone.on('pbx:transport:connected',    () => {});
phone.on('pbx:transport:disconnected', () => {});
phone.on('pbx:transport:reconnecting', () => {});
phone.on('pbx:state:reset',           () => {}); // state stale after long disconnect

System

phone.on('system:error',         ({ payload: { code, message, detail } }) => {});
phone.on('system:state:changed', ({ payload: { state, previous } }) => {});

Wildcard

onPattern handlers also receive the full envelope as a single argument (not (event, payload) — the event is the payload-bearing envelope):

phone.onPattern('call:*',  (event) => { console.log(event.type, event.payload); });
phone.onPattern('pbx:*',   (event) => { console.log(event.type, event.payload); });
phone.onPattern('*',       (event) => { console.log(event.type, event.payload); });   // every event

Call Control

// Lifecycle
await phone.connect();
await phone.disconnect();
await phone.destroy();   // permanent; create new instance to reconnect

// Calls
const call = await phone.call('1001');
await phone.answer(callId);
await phone.reject(callId);
await phone.hangup(callId);

// Mid-call
await phone.hold(callId);
await phone.unhold(callId);   // automatically holds all other active calls
await phone.mute(callId);
await phone.unmute(callId);
await phone.sendDTMF(callId, '5');

// Transfer
await phone.transfer(callId, '1002');                           // blind
await phone.transfer(callId, '1003', 'attended', consultId);   // attended

// Conference (client-side Web Audio mixing; one bridge at a time)
await phone.conference([callIdA, callIdB]);   // every call must be 'active'
await phone.leaveConference();

// State
const state = phone.getState();
const call  = phone.getCall(callId);

// History
const entries = await phone.getCallHistory();
await phone.clearCallHistory();

// Answer mode
phone.setAnswerMode('ring');             // manual answer (default)
phone.setAnswerMode('auto-answer');      // immediate answer when idle
phone.setAnswerMode('do-not-disturb');   // reject all incoming calls

Next.js Integration

SIP.js and WebRTC require a browser context — they cannot run server-side. The canonical Next.js pattern is:

// app/page.tsx
import dynamic from 'next/dynamic';

// Excludes WebPhone + entire SIP.js import graph from the server bundle
const WebPhone = dynamic(() => import('../components/WebPhone'), { ssr: false });

export default function Page() {
  return <WebPhone />;
}
// components/WebPhone.tsx
'use client';   // ← required for useState, useEffect, useRef

import { useRef, useEffect } from 'react';
import { DataVoiceWebPhone } from '@datavoice/webphone-sdk';

export default function WebPhone() {
  const phoneRef = useRef<DataVoiceWebPhone | null>(null);

  // Cleanup on unmount (and React Strict Mode double-invoke)
  useEffect(() => {
    return () => { phoneRef.current?.destroy().catch(() => {}); };
  }, []);

  // ...
}

Also add to next.config.ts as a safety net:

webpack: (config, { isServer }) => {
  if (isServer) {
    config.externals = [...(config.externals ?? []), 'sip.js', '@datavoice/webphone-sdk'];
  }
  return config;
},

HTTPS is required. Start dev with next dev --experimental-https or use mkcert. See examples/nextjs/ for the complete working example.


Blazor (.NET) Integration

For Blazor WebAssembly and Blazor Server applications, use the provided interop layer instead of importing the SDK directly. The SDK is a JavaScript library — Blazor consumes it via IJSRuntime.

Copy these files into your Blazor project:

wwwroot/js/webphone-interop.js   ← JS module bridge
WebPhoneSDK/WebPhoneService.cs   ← C# DI service
WebPhoneSDK/WebPhoneConfig.cs    ← C# config models
WebPhoneSDK/WebPhoneEvents.cs    ← C# event records
Components/WebPhonePanel.razor   ← Example Razor component

Register the service:

// Program.cs
builder.Services.AddScoped<WebPhoneService>();

Use in a component:

@inject WebPhoneService Phone

@code {
    protected override void OnInitialized()
    {
        Phone.OnRegistered    += async p => { ... };
        Phone.OnCallIncoming  += async p => await Phone.AnswerAsync(p.Call.Id);
        Phone.OnCallCompleted += async p => { ... };
    }

    private async Task ConnectAsync() =>
        await Phone.ConnectAsync(new WebPhoneConfig { Sip = new SipConfig { ... } });
}

See examples/blazor/ for the complete working example including the interop bridge, event models, and a full WebPhonePanel.razor component with call controls.


Advanced: Direct Transport Access

Bypass the facade to access the transports directly (useful for diagnostics). Note that SipJsTransport/DispatcherTransport handlers receive the event's fields directly — not the { type, timestamp, payload } envelope that DataVoiceWebPhone's public on() uses (see Events above). These are two distinct event systems.

import { SipJsTransport, DispatcherTransport } from '@datavoice/webphone-sdk';

const sip = new SipJsTransport(sipConfig, mediaConfig);
sip.on('call:tracks:remote', ({ stream }) => { audioEl.srcObject = stream; });
await sip.connect();
await sip.register();

Call Correlation

The Dispatcher's callGlobalId (format: "pbx71-1781901741.2139") is the correct cross-service identifier. The uniqueId field is per-PBX only — do not use it for cross-system correlation.

To correlate Dispatcher events with a SIP dialog, inject ${UNIQUEID} into a custom SIP header from the Asterisk dial plan:

; extensions.conf
exten => _X.,n,SIPAddHeader(X-DataVoice-CallId,${UNIQUEID})

Then read it from invitation.request.getHeader('X-DataVoice-CallId') in a custom SipTransport subclass.


Security

  • Never pass SIP passwords as URL query parameters. Use the constructor config object.
  • Never log sip.password. The SDK redacts it automatically in all internal logging.
  • TURN credentials must be time-limited. Generate them server-side using HMAC-SHA1 (RFC 8489 §9.2). Do not use static TURN credentials.
  • The SDK holds no credentials in any persistent store. All credential fields exist only in memory for the lifetime of the DataVoiceWebPhone instance.

HTTPS

getUserMedia() and WebSocket upgrade to wss:// both require a secure context. Serve this SDK from HTTPS or localhost.

Local development with mkcert:

mkcert -install
mkcert localhost 127.0.0.1
# produces: localhost-key.pem  localhost.pem

Build & Test

npm install
npm run build          # tsup → dist/index.{mjs,cjs,global.js} + .d.ts
npm run typecheck      # tsc --noEmit
npm run test           # vitest unit tests
npm run test:coverage  # coverage report

# Development example page (https://localhost:3000)
npm run dev:examples

What is not yet in this release

  • Device enumeration and explicit, consumer-triggered mid-call microphone switching UI (the audio resilience subsystem can rebind automatically on device disconnect, but there's no getDevices()/setMicrophone()-style API yet).
  • Legacy ASP.NET SignalR 1.x hub full integration (requires jquery.signalR CDN + four workspace context IDs from the DataVoice backend). The interface is complete; the test tool supports it via configurable fields.

Changelog

Full history: CHANGELOG.md

0.2.3 — 2026-07-20

  • Added AlreadyQueueMemberErrorsdk.telephony.queues.addAgent() now throws this typed error (instead of a generic ServerError) when the interface is already a member of the queue, matching the production Telephony API Gateway's actual behavior of returning 502 instead of 409 for this case. Catch alongside ConflictError for idempotent queue joins — see Queue Integration for the known limitation and full pattern. removeAgent() and other 502 responses are unaffected.

0.2.2 — 2026-07-16

  • Fixed sdk.telephony.queues.addAgent() sending Paused as a JSON number (0/1) instead of a JSON boolean — rejected 400 by any backend with a strongly typed bool Paused field. Confirmed and fixed against the production Telephony API Gateway. Audited the rest of the SDK for the same class of bug; this was isolated. Added regression tests that previously didn't exist for this module.

0.2.1 — 2026-07-13

  • Fixed all event-handling examples in this README — handlers receive the full { type, timestamp, payload } envelope, not the payload flattened onto the handler argument (e.g. ({ payload: { extension } }) => {}, not ({ extension }) => {}). Also fixed system:transport:sip:disconnected's documented field name (error, not reason) and onPattern's single-argument signature. No code changes — docs only.

0.2.0 — 2026-07-01

  • DataVoicePlatform — new REST-only facade (Users, DialPlan, Telephony/Queues modules), independent of SIP calling
  • AuthProvider hierarchy (BearerTokenAuthProvider, ApiKeyAuthProvider, NoAuthProvider) and typed API error hierarchy (ApiError + 9 subclasses)
  • Multilingual Getting Started guide (English/Spanish/Portuguese) — see docs/getting-started/

0.1.0-alpha — 2026-06-22

  • SIP registration over WSS (SIP.js 0.21.x)
  • Incoming and outgoing audio calls
  • Hold / Unhold / Mute / DTMF / Blind Transfer / Attended Transfer
  • Auto-answer and Do-Not-Disturb answer modes
  • HoldAll — automatic hold of other calls when one becomes active
  • Dispatcher hub PBX event streaming (AgentConnect / AgentComplete / Hangup)
  • Call history with pluggable storage (localStorage, IndexedDB, in-memory, null)
  • Exponential backoff reconnection for Dispatcher hub with stale-state detection
  • TypeScript declarations, ESM + CJS + IIFE builds

License

UNLICENSED — DataVoice internal / client use only.