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

@sautipbx/voice-sdk

v0.2.1

Published

Browser SDK for placing and receiving web calls on the SautiPBX voice platform. Drop in a <script> tag (or npm install in a bundler app), authenticate with an ephemeral token from your backend, and you have a working softphone.

Readme

@sautipbx/voice-sdk

Browser SDK for placing and receiving web calls on the SautiPBX voice platform. Drop in a <script> tag (or npm install in a bundler app), authenticate with an ephemeral token minted by your backend, and you have a working softphone — call, answer, hold, mute, DTMF, device selection, and rich local call events.

Status: 0.2.x, under test. API may change before 1.0.

Install

Script tag (no build step — exposes the VoiceSDK global):

<script src="https://cdn.jsdelivr.net/npm/@sautipbx/[email protected]/dist/voice.iife.js"></script>

Bundler (React / Vue / Vite / webpack):

npm install @sautipbx/voice-sdk
import { Phone } from '@sautipbx/voice-sdk';

The token flow (read this first)

Your secret API key never touches the browser. The browser only ever holds a short-lived, single-extension, revocable phone token:

  1. Your backend calls POST /api/phone-tokens/mint with your secret API key, passing the end-user's uuid. It gets back a token and an iceServers config (STUN/TURN for NAT traversal).
  2. Your backend hands only the token and iceServers to the browser.
  3. The browser passes them to phone.authenticate({ token, iceServers }).

If a token leaks it expires within minutes, works for a single extension, and can be revoked instantly with POST /api/phone-tokens/revoke.

Quickstart

<script src="https://cdn.jsdelivr.net/npm/@sautipbx/[email protected]/dist/voice.iife.js"></script>
<script>
  const phone = new VoiceSDK.Phone({ logLevel: 'info' });

  // { token, iceServers } came from YOUR backend's mint call (see above).
  await phone.authenticate({ token, iceServers });

  // Outbound
  const call = phone.call('+254711111111', { customPayload: '{"caseId":"CASE-0042"}' });
  call.on('ringing', () => console.log('ringing…'));
  call.on('accepted', () => console.log('connected'));
  call.on('ended', (r) => console.log('ended', r));

  // Inbound
  phone.on('incoming', (incoming) => {
    console.log('call from', incoming.remoteIdentity);
    incoming.answer();   // or incoming.reject()
  });
</script>

API

new Phone(options?)

| Option | Default | Notes | | ------------ | ------------------ | ----- | | logLevel | 'none' | Silent by default. Set 'error' | 'info' | 'debug' to log to the browser console (prefixed [voice-sdk]); 'debug' also enables JsSIP wire tracing. | | onLog | — | Custom log sink (level, ...args) => void. When set, emitted lines (still gated by logLevel) go here instead of the console — route them into your own UI/telemetry. | | iceGatheringTimeout | 3000 | Fallback cap (ms) on ICE gathering. Normally the call is sent the instant a TURN relay candidate is gathered (sub-second), so this only bites if no relay ever arrives — avoiding the ~39.5s stall on UDP-restricted networks. 0 disables early-send and waits for full gathering. | | iceServers | public STUN | Fallback ICE config. In production pass the per-session config from your mint response to authenticate instead. | | wssUrl | production FQDN | Override only for testing. | | realm | production realm | SIP domain. |

Methods

  • authenticate(token | { token, iceServers }) → Promise<void> — registers; resolves on success.
  • call(destination, { customPayload? }) → Call — destination is a bare extension/number or full SIP URI.
  • unregister()
  • listDevices() → { inputs, outputs }
  • setInputDevice(id) / setOutputDevice(id) / setVolume(0..1)
  • Getters: extension, account, isRegistered

Events: registered, unregistered, registrationFailed, connected, disconnected, incoming.

Capturing logs — route SDK logs into your own handler instead of the console:

const phone = new VoiceSDK.Phone({
  logLevel: 'debug',
  onLog: (level, ...args) => myLogStore.push({ level, args, at: Date.now() }),
});

Other exports

  • describeFailure(reason) — turns a Call's ended / failed reason into a one-line human summary, e.g. "402 Insufficient balance · cause=Rejected" — handy for surfacing exactly why a call was refused in your UI.
  • decodeToken(token) / isExpired(token) — inspect a phone token's claims and expiry client-side, without a network round-trip.
  • DEFAULT_ICE_SERVERS — the built-in public-STUN fallback Phone uses when you don't pass iceServers.

Call

  • answer() / reject() / hangup()
  • hold() / unhold()
  • mute() / unmute()
  • sendDigit(tone) — DTMF
  • Getters: direction, remoteIdentity, isOnHold, isMuted
  • Events: ringing, accepted, ended, failed, hold, unhold, muted, unmuted.

Events: what you get here, and what lives on your backend

The SDK surfaces local call events (the ones above) — everything a phone UI needs, observed directly in the browser. Platform-truth events that the browser can't know — call cost, recording ready + URL, billing, and bridged/far-leg state — are delivered to your backend via webhooks or a backend /api/stream subscription, where your CDR/billing logic lives. That split is deliberate: platform-truth events belong on your backend, where your own records live.

Requirements & gotchas

  • Secure context required. WebRTC mic capture only works over HTTPS (or http://localhost). On a plain http://<LAN-IP> origin, registration can succeed but calls silently fail — the SDK throws a clear error when you try to call from an insecure context.
  • NAT / TURN. The default is a public STUN server, which is enough on cooperative NATs but not for symmetric-NAT / mobile. Reliable traversal needs TURN — the mint response's iceServers includes ephemeral, per-session TURN credentials, so pass it straight to authenticate().
  • customPayload is sent as the X-Sauti-Custom-Payload INVITE header and is echoed back on every platform event and webhook for that call, so you can correlate a call with your own records automatically.

Local development

npm install
npm run typecheck   # tsc --noEmit
npm run build       # ESM + CJS + IIFE + .d.ts into dist/
npm run smoke       # verify the build artifacts + pure logic