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

voxnix

v2.1.0

Published

Headless voice SDK for the Omnix CPaaS platform. Voxnix connects the browser to your PBX over SIP/WebSocket, manages the agent status (Ready / Not Ready / AUX) through the CPaaS API, and gives you **functions + events** to build your own calling UI in any

Readme

Voxnix

Headless voice SDK for the Omnix CPaaS platform. Voxnix connects the browser to your PBX over SIP/WebSocket, manages the agent status (Ready / Not Ready / AUX) through the CPaaS API, and gives you functions + events to build your own calling UI in any stack: React, Vue, Angular, or plain HTML.

Current version: 2.1.0. Voxnix ships no UI. Upgrading from 1.x? See Migrating from 1.x.


Contents


Installation

npm install voxnix
# or
yarn add voxnix

Or use a script tag (no bundler):

<script src="https://unpkg.com/voxnix/dist/voxnix.iife.js"></script>
<script>
  const voice = OmnixWidget.init({ /* config */ });
</script>

| File | Format | Use | |-------------------|--------|------------------------------| | voxnix.js | ESM | import in bundlers | | voxnix.umd.cjs | UMD | require() / legacy loaders | | voxnix.iife.js | IIFE | <script> tag → window.OmnixWidget |


Quick start

import { OmnixWidget } from 'voxnix';

const voice = OmnixWidget.init({
  auth: {
    username:  '1001',                    // SIP extension
    pwd_pbx:   'secret',                  // SIP password
    pabx_host: 'sip.example.com',         // PBX host
    port:      8089,                      // WSS port (default 8089)
  },
  'X-CPAAS-Key-Fingerprint': 'sfp_v1_...', // CPaaS API key (agent status)
  cpaas_base_url: 'https://api-cpaas.omnix.co.id',

  onRegisterStatus: (s)    => console.log('register:', s),
  onCallStatus:     (s)    => console.log('call:', s),
  onIncomingCall:   (call) => console.log('incoming from', call.number),
});

voice.dial('1002');   // outgoing call
voice.hangup();       // end it

How it works

Registration flow

init()
  └─ WebSocket connect  wss://<sip_url | pabx_host>:<port>
       └─ SIP REGISTER  sip:<username>@<pbx_url | pabx_host>
            ├─ autoReady: true (default)
            │    └─ POST Ready to CPaaS ──ok──► register_status 'REGISTERED' + agent_status 'READY'
            │                           └─fail─► register_status { status: 'FAILED', reason }
            └─ autoReady: false
                 └─ GET extension status from CPaaS
                      ├─ already Ready ──► 'REGISTERED' + 'READY'
                      └─ otherwise     ──► 'SIP_CONNECTED' + 'NOT_READY' | 'AUX'
                                           (call setReady() to go to 'REGISTERED')

Before every agent-status change, Voxnix checks the extension on CPaaS. If CPaaS reports is_register = 0, Voxnix sends a SIP re-REGISTER and polls CPaaS (3 × 1s) until it reports the extension as registered. If it still isn't registered, the status call returns false.

register_status values

| Value | Meaning | |-------------------------------|---------| | 'CONNECTING' | Initial state (only visible via getState()) | | 'SIP_CONNECTED' | SIP registered, agent not Ready yet (autoReady: false, or after setNotReady()) | | 'REGISTERED' | SIP registered and agent Ready. Calls can be made and received | | { status: 'FAILED', reason }| See reasons below |

Typical reason values:

  • Missing SIP credentials (username, password, host)
  • WebSocket disconnected, retrying...: Voxnix keeps reconnecting on its own. When the socket comes back, the registration flow runs again.
  • SIP registration failed: <cause>: for example, a wrong password.
  • SIP Unregistered
  • Failed to hit extension ready API: the fingerprint is missing or wrong, or the CPaaS API rejected the request.
  • Extension not registered after SIP re-register

Call status flow

outgoing:  Idle ─dial()─► Calling... ─answered─► InCall ─hangup / remote BYE─► Idle
                                    └─rejected / no answer─► Idle  (+ call_failed)
incoming:  Idle ─INVITE─► Ringing ─answer()─► InCall ─► Idle
                                 └─reject() / caller cancels─► Idle (+ callUnanswered)

Only one call at a time. A second incoming call while one is active is automatically rejected with 486 Busy Here.


Config reference

OmnixWidget.init(config). There is one engine per page. Calling init() again destroys the previous instance first.

SIP credentials: auth

| Field | Type | Required | Description | |-------------|----------|----------|-------------| | username | string | ✔ | SIP extension (alias: user_pbx) | | pwd_pbx | string | ✔ | SIP password (alias: secret) | | pabx_host | string | ✔* | PBX host (aliases: pbxurl, host). http(s):// is stripped | | port | number | | WSS port, default 8089 |

* Not required when pbx_url is given.

pabxConfig: { auth: {...} } is accepted as an alternative to auth.

Hosts & endpoints

| Option | Default | Description | |------------------|----------------------------------|-------------| | pbx_url | auth.pabx_host | SIP domain used in the SIP URI sip:<username>@<pbx_url> (alias: pabxConfig.pbx_url) | | sip_url | SIP domain | WebSocket host: wss://<sip_url>:<port>. ws(s):// and a trailing / are stripped (alias: pabxConfig.sip_url) | | cpaas_base_url | https://api-cpaas.omnix.co.id | CPaaS API base URL, must be https://. A trailing / is stripped (aliases: cpaasBaseUrl, env.CPAAS_BASE_URL, pabxConfig.cpaas_base_url) |

Use these when the SIP domain, the WebSocket host and the API live on different hosts:

OmnixWidget.init({
  auth: { username: '1001', pwd_pbx: '***', pabx_host: 'sip.example.com' },
  'X-CPAAS-Key-Fingerprint': 'sfp_v1_...',
  cpaas_base_url: 'https://api.example.com/',
  pbx_url:        'pbx.example.com',     // SIP domain
  sip_url:        'wss.example.com',     // WSS host
});

X-CPAAS-Key-Fingerprint

The API key Voxnix sends as a header to the CPaaS endpoints:

GET  {cpaas_base_url}/voice/portal/extension?extension=<username>   → read status / is_register
POST {cpaas_base_url}/voice/extensions/status                       → { extension, status, reason }

Voxnix uses the first of these keys that is present:

| Priority | Key | |----------|-----| | 1 | 'X-CPAAS-Key-Fingerprint' | | 2 | fingerprint | | 3 | cpaasKeyFingerprint | | 4 | env.fingerprint | | 5 | env.X_CPAAS_Key_Fingerprint | | 6 | pabxConfig['X-CPAAS-Key-Fingerprint'] |

Without a fingerprint, every agent-status call returns false. With autoReady: true the engine then reports FAILED ("Failed to hit extension ready API") even though SIP registered fine.

Behaviour

| Option | Type | Default | Description | |-------------|--------------------------|---------|-------------| | autoReady | boolean | true | Set the agent Ready right after SIP registration. If false, the engine restores the current CPaaS status and waits for setReady() | | ringtone | boolean | true | Play the built-in ringtone on an incoming call. Set false to play your own | | contacts | { [number]: string } | — | Name lookup used for name when the caller sends no display name |

Callbacks

All callbacks are optional. Payloads are described in Events & callbacks.

| Option | Called when | |--------------------|-------------| | onRegisterStatus | Registration becomes 'REGISTERED' or FAILED. SIP_CONNECTED is not reported here; use on('register_status') to see it | | onAgentStatus | Agent status changes | | onCallStatus | Call status changes | | onCallFailed | An outgoing call fails (rejected, busy, unreachable, …) | | onIncomingCall | An incoming call starts ringing | | onCallAnswered | A call (either direction) is connected | | onCallEnded | A connected call ends | | onCallUnanswered | An incoming call ends without being answered (missed, rejected, caller cancelled) |


Instance API

init() returns:

| Method | Returns | Description | |--------|---------|-------------| | dial(number) | — | Start an outgoing call to sip:<number>@<SIP domain>. Ignored if another call is active. Invalid characters → call_failed 'Invalid number' | | answer() | — | Answer the ringing incoming call | | reject() | — | Reject the ringing incoming call (same as hangup()) | | hangup() | — | End the current call. Sends CANCEL while dialing, rejects while ringing, BYE when connected | | mute(muted = true) | — | true mutes the microphone, false unmutes it. Only during a connected call | | hold(onHold = true) | — | true holds the call, false resumes it. Only during a connected call | | sendDtmf(tones) | — | Send DTMF during a connected call. Accepts one tone or a string: '5', '123#' | | transfer(target, { attended = false }) | Promise<boolean> | (2.1+) Transfer the connected call to target. Blind: REFER straight away. Attended: hold the caller, call target, and REFER (Replaces) once it answers. On success our leg is hung up (onCallEnded fires) | | setReady() | Promise<boolean> | Agent → Ready. Emits 'REGISTERED' + 'READY' on success | | setNotReady() | Promise<boolean> | Agent → Not Ready (CPaaS Logout). Emits 'SIP_CONNECTED' + 'NOT_READY' | | setAux(code = 'AUX', label = code) | Promise<boolean> | Agent → AUX. code is sent to CPaaS as the reason; label is for your UI | | getState() | object | Snapshot of the current state, see State | | on(event, cb) | () => void | Subscribe to an event. Returns an unsubscribe function | | destroy() | — | End any call, unregister, close the WebSocket, and drop all listeners |

The call controls are safe to call at any time. Outside a valid state they do nothing.


State

getState() returns a fresh copy each time:

{
  registerStatus: 'REGISTERED',   // 'CONNECTING' | 'SIP_CONNECTED' | 'REGISTERED' | 'FAILED'
  agentStatus:    'READY',        // 'NOT_READY' | 'READY' | 'AUX'
  currentAux:     null,           // { code, label } while AUX
  callStatus:     'InCall',       // 'Idle' | 'Calling...' | 'Ringing' | 'InCall'
  callDuration:   '01:23',        // 'MM:SS' or 'HH:MM:SS'
  isMuted:        false,
  isOnHold:       false,
  remote:         { number: '1002', name: 'Jane', direction: 'outgoing', headers: {} }, // null when Idle
}

Use it to draw your UI when it first mounts, then keep it in sync with events.


Events & callbacks

Subscribe with voice.on(event, cb). Events that have a callback option fire that callback too.

| Event | Payload | Callback | |------------------------|---------|----------| | register_status | 'SIP_CONNECTED' | 'REGISTERED' | { status: 'FAILED', reason } | onRegisterStatus (skips SIP_CONNECTED) | | agent_status | 'READY' | 'NOT_READY' | 'AUX' (getState().currentAux holds the AUX code) | onAgentStatus | | call_status | 'Idle' | 'Calling...' | 'Ringing' | 'InCall' | onCallStatus | | call_duration | '00:42', every second while connected, '00:00' on end | — | | call_failed | cause string, e.g. 'Busy', 'Rejected', 'Unavailable' | onCallFailed | | incoming_call | { number, name, direction: 'incoming', headers } | onIncomingCall | | sync_call_answered | { number, name, direction, headers } | onCallAnswered | | sync_call_ended | { number, name, direction, headers, cause, originator } | onCallEnded | | sync_call_unanswered | { number, name, direction, headers, cause, originator } | onCallUnanswered | | mute_status | boolean | — | | hold_status | boolean | — |

  • direction: 'incoming' or 'outgoing'.
  • headers (2.1+): custom X-* headers of an incoming INVITE, e.g. { 'X-Call-Uuid': '…', 'X-Source-Type': 'whatsapp-call', 'X-Queue-Name': '…' }. Always {} for outgoing calls. These values come from the caller side, so treat them as untrusted.
  • originator: who ended the call. 'local' means you, 'remote' means the other side, 'system' means a network or SIP error.
  • cause: the end reason, such as 'Terminated', 'Canceled', 'Rejected', 'Busy' or 'No Answer'.

Recipes

Outgoing call

voice.on('call_status', (s) => render(s));
voice.on('call_duration', (d) => (timer.textContent = d));
voice.on('call_failed', (cause) => toast(`Call failed: ${cause}`));

callBtn.onclick   = () => voice.dial(input.value);
hangupBtn.onclick = () => voice.hangup();
muteBtn.onclick   = () => voice.mute(!voice.getState().isMuted);
holdBtn.onclick   = () => voice.hold(!voice.getState().isOnHold);
keypad.onclick    = (e) => voice.sendDtmf(e.target.dataset.key);

Incoming call

const voice = OmnixWidget.init({
  ...config,
  onIncomingCall:   ({ number, name }) => showRinging(name || number),
  onCallAnswered:   () => showInCall(),
  onCallEnded:      ({ cause }) => showIdle(),
  onCallUnanswered: ({ number }) => addMissedCall(number),
});

answerBtn.onclick = () => voice.answer();
rejectBtn.onclick = () => voice.reject();

Agent status (manual Ready)

const voice = OmnixWidget.init({ ...config, autoReady: false });

voice.on('register_status', (s) => {
  if (s === 'SIP_CONNECTED') showSetReadyButton();   // connected, agent not Ready
  if (s === 'REGISTERED')    showDialer();
});

readyBtn.onclick    = async () => { if (!(await voice.setReady())) toast('Failed to set Ready'); };
notReadyBtn.onclick = () => voice.setNotReady();
breakBtn.onclick    = () => voice.setAux('BRK', 'Break');

React hook

import { useEffect, useRef, useState } from 'react';
import { OmnixWidget } from 'voxnix';

export function useVoice(config) {
  const ref = useRef(null);
  const [state, setState] = useState(null);

  useEffect(() => {
    const voice = OmnixWidget.init(config);
    ref.current = voice;
    const sync = () => setState(voice.getState());
    const events = ['register_status', 'agent_status', 'call_status', 'call_duration', 'mute_status', 'hold_status'];
    events.forEach((e) => voice.on(e, sync));
    sync();
    return () => voice.destroy();
  }, []); // init once

  return [state, ref];
}

// const [state, voice] = useVoice(config);
// <button onClick={() => voice.current.dial('1002')}>Call</button>

Vue 3

import { onMounted, onUnmounted, ref } from 'vue';
import { OmnixWidget } from 'voxnix';

export function useVoice(config) {
  const state = ref(null);
  let voice;
  onMounted(() => {
    voice = OmnixWidget.init(config);
    const sync = () => (state.value = voice.getState());
    ['register_status', 'agent_status', 'call_status', 'call_duration', 'mute_status', 'hold_status']
      .forEach((e) => voice.on(e, sync));
    sync();
  });
  onUnmounted(() => voice?.destroy());
  return { state, dial: (n) => voice.dial(n), hangup: () => voice.hangup() };
}

Browser requirements

  • HTTPS (or localhost): browsers only give microphone access on secure origins.
  • Microphone permission: the browser asks for it on the first dial() or answer(). If the user denies it, the call fails (call_failed, or onCallUnanswered for an incoming call).
  • Remote audio: Voxnix creates a hidden <audio id="voxnix-remote-audio"> in document.body. Autoplay rules may block it until the user has clicked something on the page. Answering or dialing from a click handler counts as that interaction.
  • Ringtone: generated with WebAudio, which has the same user-interaction rule. Set ringtone: false to use your own sound.

Security

  • Treat caller data as untrusted. name comes from the caller's SIP display name and number from their SIP URI. Render them with textContent (or your framework's default escaping), never innerHTML.
  • Credentials live in the browser. The SIP password and X-CPAAS-Key-Fingerprint are readable by anyone with devtools on that page. Fetch them from your backend after the agent logs in, use per-agent SIP credentials, and never hardcode them in frontend code.
  • HTTPS only. cpaas_base_url must be https:// (http://localhost is allowed for development). Any other URL disables CPaaS API calls, and agent-status methods return false.
  • Input validation. dial() only accepts 0-9 A-Z a-z + * # . _ -. Anything else emits call_failed with 'Invalid number'.
  • Voxnix never logs credentials or API responses to the console.

Sample page

examples/index.html is a plain-HTML page that loads the script-tag build. It calls OmnixWidget.init() with a config object, has buttons for every API method, and shows a live getState() view and an event log.

npm run build
python3 -m http.server 5599     # from the repo root
# open http://localhost:5599/examples/

Edit the CONFIG object at the top of the script to point it at your PBX.


Build

npm install
npm run build        # ESM + UMD + IIFE into dist/
npm run build:lib    # ESM + UMD only
npm run build:iife   # IIFE only
npm run watch        # rebuild ESM + UMD on change

Migrating from 1.x

  • The UI is gone. There is no floating button or dialer panel. The options placement, offset, container, showAuxButton and auxOptions are ignored. Build the UI with the Instance API and events.
  • New SIP engine. Call callbacks now receive { number, name, direction, ... } instead of a raw SIP session object.
  • hold(true) now holds and hold(false) resumes (1.x had them reversed).
  • onCallEnded fires once per call. Before, it could fire twice for incoming calls.
  • Call payloads include headers (2.1+), the X-* headers of an incoming INVITE. Use it instead of reading headers from the raw SIP session.
  • Call transfer is transfer(target, { attended }) (2.1+) instead of refer() on the raw session.
  • New in 2.0: sendDtmf, setReady, setNotReady, setAux, getState, on, plus the options onCallStatus, onCallFailed, ringtone and contacts.
  • contacts has no built-in demo entries any more. Pass your own map.

Changelog

v2.1.0

  • transfer(target, { attended }): blind transfer (REFER), or attended (hold caller, consult call, REFER with Replaces)
  • Call payloads and getState().remote include headers: the X-* headers of an incoming INVITE (X-Call-Uuid, X-Source-Type, X-Queue-Name, …)
  • Build runs on prepack, so npm pack / npm publish always ship a fresh dist

v2.0.0

  • Removed the built-in React UI; Voxnix is now a headless SDK (no React / Tailwind dependencies)
  • New internal SIP engine
  • New API: sendDtmf, setReady, setNotReady, setAux, getState, on; options onCallStatus, onCallFailed, ringtone, contacts
  • Configurable cpaas_base_url, pbx_url (SIP domain), sip_url (WSS host)
  • Re-REGISTER + CPaaS poll when the extension reports is_register = 0
  • Security: credentials and API responses are no longer logged (1.0.23–1.0.24 logged the full config); HTTPS-only CPaaS URL; dial() input validation; build before publish
  • Fixes: cancel while dialing, double onCallEnded, inverted hold(), outgoing call duration and callbacks, busy second incoming call, destroy() not stopping SIP, IIFE global OmnixWidget.init

v1.0.12

  • Rollback DevToast / console interceptor feature

v1.0.11 and earlier

  • Agent status bar (Ready / Not Ready / AUX) with configurable auxOptions
  • onAgentStatus callback
  • autoReady and showAuxButton config options
  • Keyboard shortcut support for dialer and DTMF
  • React 17 / 18 compatibility layer
  • Dual-SIP auth shape support (auth and pabxConfig.auth)