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

@didlogic/voice-sdk

v1.6.0

Published

DIDLogic Voice SDK — drop-in WebRTC dialer widget and headless SIP client for browser-based softphone integration.

Readme

@didlogic/voice-sdk

Drop-in WebRTC dialer widget and headless SIP client for embedding on a DIDLogic partner's website. The partner's customers place calls straight from the page; the SIP infrastructure and billing live on the DIDLogic side, under the partner's account.

Contents

What each version adds

Every section below names the release it arrived in (Since 1.x.0); anything without a marker works on every version. Check the version you have installed before relying on a feature:

npm ls @didlogic/voice-sdk

| Version | Released | Adds | | ------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------- | | 1.6.0 | 2026-10-07 | Call history: <dl-call-history> widget, history proxy command, fetchCallHistory, CommandError, Dialer.dial | | 1.5.0 | 2026-09-16 | Caller ID selection: caller_ids proxy command, caller-id attribute, Settings selector, fetchCallerIds, Device.callerId | | 1.4.0 | 2026-09-06 | Dialer.sessionProvider; built-in incoming call screen (incoming-calls); visible transport loss and registration retry | | 1.3.0 | 2026-09-05 | DeviceOptions.sessionProvider — passwordless registration from short-lived session grants | | 1.2.0 | 2026-07-23 | DeviceOptions.audioProcessing — echo cancellation, noise suppression, auto gain, mono | | 1.1.0 | 2026-07-21 | Inbound calls: Device incomingCall event, VoiceCall.answer() / reject(), calledNumber, <dl-dialer> inboundCallHandler | | 1.0.1 | 2026-05-28 | number attribute to prefill the dialpad | | 1.0.0 | 2026-05-07 | <dl-dialer> widget, headless Device / VoiceCall, CallCredentialsManager, Click-to-Call, theming |

Inbound calls work from 1.1.0 with your own UI (inboundCallHandler on the widget, or incomingCall on Device). The ready-made Answer/Reject screen needs 1.4.0. The full history is in CHANGELOG.md.

Architecture

   ┌──────────┐   POST {command:"call"}   ┌────────────────┐   POST + api_token   ┌──────────────────┐
   │ Browser  │ ────────────────────────► │ Your backend   │ ───────────────────► │   DIDLogic API   │
   │ <dl-...> │                           │  (proxy)       │                      │  /mobile/api/sdk │
   └──────────┘ ◄──────────────────────── └────────────────┘ ◄─────────────────── └──────────────────┘
                {session_id, session_pwd,                     {session_id, session_pwd,
                 wss_url, expires_in}                          wss_url, expires_in}

The key principle: the api_token (a.k.a. SdkApiToken) lives on your backend only. The widget talks exclusively to your proxy; the proxy adds the token and forwards the request to DIDLogic. The widget gets back ephemeral SIP credentials (TTL ~1 hour), registers over WSS, and places the call.

Install

npm install @didlogic/voice-sdk

Quickstart — ESM

import '@didlogic/voice-sdk'

document.body.innerHTML = `
  <dl-dialer
    proxy-url="https://your-backend.example.com/webrtc"
    brand-name="Acme Phone"
  ></dl-dialer>
`

The side-effect import registers every custom element. After that, <dl-dialer> renders from any framework as a regular web component. In the legacy credential mode, the proxy-url attribute is required — it points to your backend endpoint that issues short-lived SIP credentials (see Setup on your backend).

Quickstart — React

import '@didlogic/voice-sdk'

export function PhoneWidget() {
  return <dl-dialer proxy-url="https://your-backend.example.com/webrtc" brand-name="Acme Phone" />
}

The package automatically augments JSX.IntrinsicElements for React 18 (global JSX) and React 19 (the react module's JSX), so <dl-dialer> is recognised in TSX with no extra declaration.

For other JSX runtimes (Vue / Svelte / Solid / Preact), add your own augmentation against the widget's exported classes.

PBX session provider

Since 1.4.0 on <dl-dialer>; DeviceOptions.sessionProvider for the headless Device since 1.3.0.

sessionProvider is a JavaScript property of type WebRtcSessionProvider. It takes precedence over proxy-url: the widget creates Device({ sessionProvider }) and never starts CallCredentialsManager in this mode. Voice Core obtains a fresh one-use grant on startup and reconnect, and validates the grant before connecting. No SIP password is required.

import { Dialer } from '@didlogic/voice-sdk'
import type { WebRtcSessionGrant, WebRtcSessionProvider } from '@didlogic/voice-sdk'

const sessionProvider: WebRtcSessionProvider = async () => {
  const response = await fetch('/demo/session', {
    method: 'POST',
    credentials: 'same-origin',
    cache: 'no-store',
    signal: AbortSignal.timeout(10_000),
  })
  if (!response.ok) throw new Error(`Connection unavailable (${response.status})`)
  return (await response.json()) as WebRtcSessionGrant
}

const dialer = new Dialer()
dialer.brandName = 'PBX phone'
dialer.sessionProvider = sessionProvider
dialer.incomingCalls = true
document.body.append(dialer)

/demo/session is an example host-owned BFF route, not an SDK endpoint. The BFF holds the PBX L4 key, fixes the permitted tenant/user and application Origin, and returns only the grant fields id, sip_uri, wss_url, connect_before with Cache-Control: no-store. The provider must return a newly issued grant on every invocation, use a bounded request, and throw a sanitized error on failure. Never cache grants or keys in logs or browser persistent storage.

The provider can be assigned before mounting or after element creation. Lit coalesces configuration changes and maintains one Device for the effective auth configuration. An identical provider assignment, brand update, or ignored proxy-url update does not reconnect. Changing the provider function, changing the effective legacy proxy URL, or switching auth modes ends the old session and starts a new Device. Use a stable provider function (for example useCallback in React 19); React 18 assigns it through a ref.

Removing the element destroys its Device, detaches widget-owned call listeners and cancels delayed call-screen resets. Reattaching starts a fresh session; an obsolete pending grant cannot create a connection. A provider request that is already in flight may finish, but Core discards its result. The provider owns its HTTP timeout. Registration failures show an error and Retry uses Core revalidation to request a fresh grant. A transport connection alone never shows Ready; SIP registration does.

To receive calls in this mode, see Receiving calls with the widget.

Setup on the DIDLogic portal

To let the widget place calls, you need to create an SDK token in your DIDLogic account — a long-lived bearer token your backend will inject into requests.

What an SDK token is

  • 1 token = 1 DIDLogic user (the user calls are placed and billed against).
  • Each token gets its own SIP account, created with the token and labelled SDK Instance: <token name> in the portal. Every widget or Device started with that token registers as this account.
  • Can be enabled / disabled / revoked at any time — deactivation tears down active sessions immediately.

How to get one

In the DIDLogic portal open API & Webhooks → SDK Tokens and create a token. The token value is shown once; copy it straight into your backend secrets.

Once you have the token:

  1. Put it in your backend secrets (env, vault — anything, as long as it's not in the browser and not in git).
  2. Make sure the user the token belongs to has an active DID with outbound calling enabled.
  3. DIDLogic-side rate limits:
    • 60 requests per minute per token
    • 120 requests per minute per IP Exceeding either → 429 Too Many Requests.
  4. If the user is blocked (negative balance, suspended), DIDLogic returns 403 Forbidden.

How API errors surface in the widget

| HTTP from DIDLogic | What <dl-dialer> shows | | ----------------------- | --------------------------------------------------------------- | | 401 Unauthorized | "Authentication failed" (token invalid / revoked) | | 403 Forbidden | "Access denied" (user blocked, limits) | | 429 Too Many Requests | "Too many requests, please retry later" | | Any 5xx | Auto-retry with exponential backoff (cap 5 min, up to 10 tries) |

Setup on your backend (proxy)

The proxy is a single HTTP endpoint on your side that:

  1. Accepts a POST from the widget with JSON { "command": "call" } — or { "command": "caller_ids" }, which the widget sends after the credentials to fill its Caller ID selector (again on the next credentials refresh if it failed). Your own code may also send { "command": "history" } (since 1.6.0) to read call history.
  2. Adds an api_token field (value from env) to the body.
  3. Forwards the request to https://<DIDLogic-host>/mobile/api/sdk/command.
  4. Returns the DIDLogic response as is.

Contract

From the widget to your proxy:

POST /webrtc        HTTP/1.1
Content-Type: application/json

{ "command": "call" }

From your proxy to DIDLogic:

POST /mobile/api/sdk/command        HTTP/1.1
Content-Type: application/json

{ "command": "call", "api_token": "<SDK_API_TOKEN>" }

Add "agent_uid": "<your user's id>" to name which of your end-users this session belongs to, so one number can ring one of them instead of every browser holding the token. It is 1 to 32 letters, digits, underscores or hyphens, and it is refused rather than trimmed. The widget never sends it: your backend knows who is logged in, the browser does not.

The pairing is exact in both directions. A session that names an agent receives only calls whose route names the same agent, and a session that names none receives only calls whose route names none — so a number stays unreachable until one side matches the other.

DIDLogic response (forwarded to the widget verbatim):

{
  "session_id": "<32 hex characters>",
  "session_pwd": "<short-lived secret>",
  "wss_url": "<WebSocket URL to register on>",
  "expires_in": 3600
}

Use wss_url exactly as returned rather than hardcoding a host: DIDLogic chooses where the session registers.

The caller_ids command (since 1.5.0) — same request shape with { "command": "caller_ids" }; the response lists the numbers the SDK token's token may present — the DIDs you purchased on DIDLogic (every number you own, verified foreign ones included, when the account has CLI passthrough):

{
  "caller_ids": [
    { "callerid": "15551234567", "country": "United States", "area": "New York" },
    { "callerid": "442012345678", "country": "United Kingdom", "area": null }
  ]
}

A proxy that allow-lists commands must admit caller_ids; when it does not, the widget simply shows no Caller ID selector.

The history command (since 1.6.0). The widget never sends it; fetchCallHistory does, from your own UI. It returns the calls of the token's SIP account, newest first, one page at a time:

POST /webrtc        HTTP/1.1
Content-Type: application/json

{ "command": "history", "type": "all", "limit": 25, "before": "<next_before of the previous page>" }

type is all (default), outbound, inbound or missed. limit is 10, 25 (default), 50 or 100. Omit before for the newest calls. Optional filters: number finds calls whose number contains those digits (in from, to or the DID), and from / to are ISO 8601 date-times bounding the call start.

{
  "calls": [
    {
      "id": "[email protected]",
      "direction": "outbound",
      "status": "answered",
      "from": "15551234567",
      "to": "442012345678",
      "did_number": null,
      "started_at": "2026-10-07T10:30:00Z",
      "duration": 120
    }
  ],
  "next_before": "eyJ0cyI6MTc5MTM2ODIwMCwiaWRzIjpbXX0"
}
  • id is the call's callid as your webhooks carry it: encrypted for an inbound call, plain for an outbound one. It is null when it cannot be given.
  • status is answered, missed (inbound, not answered) or not_connected (outbound, not connected). outbound lists only connected outgoing calls and inbound only answered incoming ones; unanswered incoming calls are under missed, and all holds every call.
  • did_number is null for a call that did not come in on one of your DIDs.
  • next_before is opaque: pass it back unchanged. It is null on the last page.
  • History belongs to the token, not to an agent: every session of a token sees all of its calls, whatever agent_uid it named.

history errors follow DIDLogic's public API error format, one object with a machine-readable code:

{
  "error": {
    "code": "validation_failed",
    "message": "The request contains invalid parameters",
    "details": [
      {
        "code": "not_included",
        "field": "limit",
        "message": "limit must be one of 10, 25, 50, 100"
      }
    ]
  }
}

| Status | code | When | | ------ | -------------------------- | ----------------------------------------------------- | | 422 | validation_failed | type, limit or before is invalid; see details | | 409 | sip_account_missing | The token has no SIP account | | 503 | call_history_unavailable | Call history is temporarily unavailable; retry |

Refusals that happen before any command runs (401, 403, 429) keep the { "error": "<message>" } shape every command shares.

Reference proxy in Node.js (zero dependencies)

// proxy.mjs — node 18+
import { createServer } from 'node:http'

const PORT = process.env.PORT ?? 3000
const SDK_API_TOKEN = process.env.SDK_API_TOKEN
const UPSTREAM = process.env.UPSTREAM_URL ?? 'https://app.didlogic.com/mobile/api/sdk/command'
const ALLOW_ORIGIN = process.env.ALLOW_ORIGIN ?? '*'

if (!SDK_API_TOKEN) {
  console.error('SDK_API_TOKEN env is required')
  process.exit(1)
}

const cors = {
  'Access-Control-Allow-Origin': ALLOW_ORIGIN,
  'Access-Control-Allow-Headers': 'Content-Type',
  'Access-Control-Allow-Methods': 'POST, OPTIONS',
}

createServer(async (req, res) => {
  if (req.method === 'OPTIONS') return res.writeHead(204, cors).end()
  if (req.method !== 'POST' || req.url !== '/webrtc') {
    return res.writeHead(404, cors).end()
  }

  const chunks = []
  for await (const chunk of req) chunks.push(chunk)
  const body = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}')

  const upstream = await fetch(UPSTREAM, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ ...body, api_token: SDK_API_TOKEN }),
  })

  const text = await upstream.text()
  res.writeHead(upstream.status, {
    'Content-Type': upstream.headers.get('content-type') ?? 'application/json',
    ...cors,
  })
  res.end(text)
}).listen(PORT, () => console.log(`proxy on :${PORT}/webrtc`))

Run it:

SDK_API_TOKEN=<token> ALLOW_ORIGIN=https://your-app.example.com node proxy.mjs

⚠️ Never put SDK_API_TOKEN in the browser. Not in an HTML attribute, not in a JS bundle, not in a frontend env file. Any leak of the token into client code = compromise. Keep it in server-side env only.

What the proxy must do (minimum)

  • Accept only POST on its public path.
  • Handle the CORS preflight (OPTIONS) for the domain hosting the widget.
  • Inject api_token from a server-side secret.
  • Forward DIDLogic's body and status code as is — otherwise the widget can't handle 401/403/429 correctly.

What the proxy may do (optional)

  • Authenticate the call via your existing session cookie / JWT — to issue SIP credentials only to logged-in users.
  • Log, rate-limit per your own users, or rewrite command based on business rules — call and caller_ids are the two commands the widget sends; history is sent by fetchCallHistory.
  • Audit who's hitting the widget and how often.

Routing numbers from your backend

Your backend can point your numbers at your agents itself, instead of anyone opening the DIDLogic portal. These are not proxy commands and the widget never calls them — they live on their own path, /mobile/api/sdk/routes, authenticated with the same X-Sdk-Api-Token header:

GET    /mobile/api/sdk/routes        # every route of this token's account
POST   /mobile/api/sdk/routes        # { "number": "19995550334", "agent_uid": "operator-7" }
DELETE /mobile/api/sdk/routes/:id    # the id a create or a list returned

A create answers 201 with { "id", "number", "agent_uid" }. Omit agent_uid and the number rings the sessions that named no agent. Adding a route joins the number's existing destinations rather than replacing them, so a number can ring several agents, and each of them is removed on its own.

Refusals come back 422 with an error string: a number that is not yours is Number not found, the same number and agent twice is Route already exists, a malformed agent_uid says so, and an id belonging to another token is Route not found.

Keep these off the path your proxy forwards. A proxy relays whatever command the browser sends, so a write placed there would be reachable from an operator's devtools with a token that is meant to be yours alone.

<dl-dialer> attributes

| Attribute | Type | Default | Since | Description | | ------------------------ | --------- | --------- | ----- | -------------------------------------------------------------------- | | proxy-url | string | "" | 1.0.0 | Required without sessionProvider; URL of your proxy endpoint | | brand-name | string | "Phone" | 1.0.0 | Title shown in the widget header | | click-to-call | boolean | false | 1.0.0 | Scan the host page and intercept clicks on tel: links | | click-to-call-selector | string | "" | 1.0.0 | CSS selector limiting the click-to-call scan area | | number | string | "" | 1.0.1 | Optional initial number to prefill the dialpad on first paint | | incoming-calls | boolean | false | 1.4.0 | Enable built-in incoming Answer/Reject UI | | caller-id | string | "" | 1.5.0 | Number to present on outgoing calls; overrides the remembered choice |

JavaScript-only properties (assign them on the element, not as attributes):

| Property | Type | Since | Description | | -------------------- | ------------------------------------ | ----- | ------------------------------------------------------------ | | inboundCallHandler | InboundCallHandler \| null | 1.1.0 | Receive inbound calls in your own UI | | sessionProvider | WebRtcSessionProvider \| undefined | 1.4.0 | Passwordless registration; takes precedence over proxy-url |

Caller ID selection

Since 1.5.0.

The widget's Settings tab offers the numbers the caller_ids command returned. The choice is put in the SIP From of every call placed afterwards; DIDLogic's outbound proxy presents it only when it is a DID you purchased and otherwise picks one of the account's caller IDs at random — so a number you do not hold is never presented. "Any of the account" leaves the pick to the proxy.

The choice is remembered in the browser's localStorage under dl-dialer:caller-id:<proxy-url>, so an operator keeps it across reloads and credential refreshes; a caller-id attribute overrides it, and a remembered number that is no longer yours is dropped. To reset, remove that key or set the attribute.

Caller ID without the widget

Since 1.5.0.

The selector is a convenience, not the API. fetchCallerIds is the same caller_ids command the widget sends, and it takes your transport:

import { CallerIdsError, fetchCallerIds } from '@didlogic/voice-sdk'

const controller = new AbortController()
try {
  const ids = await fetchCallerIds('/webrtc', {
    signal: controller.signal,
    fetch: myFetchWithAuthHeaders,
  })
  device.callerId = ids[0].callerid
} catch (err) {
  if (err instanceof CallerIdsError && err.status === 401) reauthenticate()
}

CallerIdsError carries status (absent on a network failure) and the message the proxy returned; an abort surfaces as the browser's own AbortError, not as a CallerIdsError. device.callerId keeps the digits of what you assign ('+1 (555) 111-0002' reads back as '15551110002'), clears on null or blank, and throws a TypeError on a value with no digits in it. Nothing obliges you to use CallerIdChoice — remember the operator's pick wherever your app keeps its state.

For per-element autocomplete in IDEs that consume the Custom Elements Manifest (VS Code + lit-plugin, WebStorm, Storybook), use the custom-elements.json shipped with the package.

Events

<dl-dialer> is self-contained: it owns its UI state and talks to the SIP stack internally. No custom events are emitted on the element itself.

If you need call lifecycle events (registered, accepted, ended, failed, …), use the Headless API. Device and VoiceCall expose typed event emitters.

Receiving calls with the widget

The widget refuses inbound calls with 486 Busy Here until you choose how they are shown. There are two ways; if both are set, the handler wins.

| Option | Since | Who draws the ringing UI | | -------------------------- | ----- | --------------------------------------------------------- | | inboundCallHandler | 1.1.0 | You — your own ringing screen, ringtone and notifications | | incoming-calls attribute | 1.4.0 | The widget — a built-in screen with Answer and Reject |

Calls reach the widget only when the DID is routed to the token's SIP account: in the portal, give the number a destination of type SIP account and pick SDK Instance: <token name> (after saving it shows as SDK). A Webphone destination rings the DIDLogic web phone, not your widget, and a destination pointing at another token's account rings only the widgets started with that token.

A route may also name one of your end-users, and then it rings only the session that named the same one — see Routing numbers from your backend, which also lets you create and remove routes without the portal.

Built-in incoming call screen

Since 1.4.0.

<dl-dialer proxy-url="https://your-backend.example.com/webrtc" incoming-calls></dl-dialer>

Set incoming-calls (HTML) or incomingCalls = true (JavaScript) to show the built-in incoming call screen with Answer and Reject. Answer uses the selected microphone; answering disables repeated accepts while retaining Hang up. Connected calls reuse the existing duration, mute, keypad and hangup controls. Microphone and call failures remain visible in the header.

Your own incoming call UI

Since 1.1.0.

Assign inboundCallHandler on the element to own the incoming UI. It takes priority over the built-in incoming-calls screen.

const dialer = document.querySelector('dl-dialer')

dialer.inboundCallHandler = (call) => {
  // Your ringing UI, ringtone and notifications.
  showRinging({ from: call.remoteIdentity, to: call.calledNumber })

  answerButton.onclick = () => call.answer()
  rejectButton.onclick = () => call.reject()

  call.on('accepted', () => showInCall())
  call.on('ended', (e) => hideCallUi(e.cause))
  call.on('failed', (e) => hideCallUi(e.cause))
}

inboundCallHandler is a property, not an attribute — assign it on the element instance. It is read when a call arrives, not when the widget starts, so it does not matter whether you assign it before or after the SDK module loads.

In React

React 19 assigns unknown props on custom elements as properties, so the JSX form works directly:

<dl-dialer
  proxy-url="https://your-backend.example.com/webrtc"
  inboundCallHandler={(call) => showRinging(call)}
/>

React 18 and earlier have no property path for custom elements — every prop goes through setAttribute and is stringified, so a function silently never arrives. There, use a ref:

const ref = useRef<Dialer>(null)

useEffect(() => {
  const el = ref.current
  if (!el) return
  el.inboundCallHandler = (call) => showRinging(call)
  return () => {
    el.inboundCallHandler = null
  }
})

return <dl-dialer ref={ref} proxy-url="https://your-backend.example.com/webrtc" />

Note the effect has no dependency array — it reassigns on every render, which is both cheap and correct here because the widget reads the handler when the call arrives. With [] the handler would capture the first render's state and go stale.

The property is deliberately not called onInboundCall: React treats any custom-element prop starting with on as an event registration, stripping the prefix and calling addEventListener instead of assigning the property — so an on* name would silently never be wired up, with no error or warning.

The call you get is the same VoiceCall the headless API hands out; everything in Receiving calls below applies, including that both ended and failed are terminal.

Call history widget

Since 1.6.0.

<dl-call-history> shows the token's call history, newest first, with tabs for All, Outbound, Inbound and Missed, a number search, a date range and a Load more button. It reads through the same proxy as <dl-dialer>, using fetchCallHistory, so the proxy must forward the history command.

<dl-dialer id="phone" proxy-url="https://your-backend.example.com/webrtc"></dl-dialer>
<dl-call-history
  proxy-url="https://your-backend.example.com/webrtc"
  dialer="phone"
></dl-call-history>

| Attribute | Type | Default | Since | Description | | ----------- | -------- | ------- | ----- | ---------------------------------------------------------------------------- | | proxy-url | string | "" | 1.6.0 | Required; URL of your proxy endpoint | | dialer | string | "" | 1.6.0 | id of a <dl-dialer> to call back through when a row is clicked; optional |

Clicking a row dispatches a dl-call-request event (detail: { number }, bubbling and composed) with the other party's number. With dialer set, the widget also calls that number on the linked <dl-dialer> through its public dial(number) method (since 1.6.0), which you can call yourself as well. refresh() reloads the first page, for example after a call ends.

The widget uses the same theme variables as <dl-dialer> (see Theming).

Theming

CSS custom properties applied to <dl-dialer> or any of its ancestors:

| Variable | Default | Purpose | | --------------------- | ---------------------------------------- | ------------------------------ | | --dl-primary | #447bf2 | Accent (digits on the display) | | --dl-success | #66d391 | Call button background | | --dl-danger | #ed2643 | Hangup button background | | --dl-danger-hover | #d41e38 | Hangup hover state | | --dl-bg | #ffffff | Widget background | | --dl-surface | #f0f0f0 | Inner surface tint | | --dl-surface-hover | #e4e4e4 | Inner surface hover | | --dl-text | #121011 | Primary text | | --dl-text-secondary | #9298a3 | Secondary text | | --dl-border | #e7eaf0 | Borders, separators | | --dl-font-family | KlavikaBasicRegular, Arial, sans-serif | Default UI font | | --dl-font-mono | PTMonoRegular, Arial, serif | Monospace (digits) | | --dl-radius | 6px | Default corner radius | | --dl-radius-round | 50% | Round button radius | | --dl-width | 360px | Widget width | | --dl-height | 680px | Widget height | | --dl-sheet-height | 100% | Bottom-sheet (DTMF) height |

dl-dialer {
  --dl-primary: #ff6b00;
  --dl-success: #00aa44;
  --dl-width: 320px;
  --dl-height: 600px;
}

The widget ships with its own fallback fonts. To restyle, override --dl-font-family / --dl-font-mono with your own stack.

Headless API

For non-widget integrations the SDK exposes the underlying classes:

import { Device, CallCredentialsManager } from '@didlogic/voice-sdk'

const credentials = new CallCredentialsManager({
  proxyUrl: 'https://your-backend.example.com/webrtc',
})
const device = new Device()

credentials.onCredentials = (creds) => device.applyCredentials(creds)
credentials.onError = (err) => console.error('credentials error', err)
await credentials.fetch()

device.on('registered', () => console.log('SIP registered'))
device.on('registrationFailed', (e) => console.error('register failed:', e.cause))

device.callerId = '15551234567' // since 1.5.0: one of the account's caller IDs; null = the proxy's pick
const call = await device.call('+15551234567')
call.on('accepted', () => console.log('connected'))
call.on('ended', () => console.log('hung up'))
call.on('failed', () => console.log('call failed'))

call.mute()
call.sendDTMF('5')
call.hangup()

Receiving calls

Since 1.1.0. Before it, inbound calls were refused with 405 Method Not Allowed.

Subscribe to incomingCall and the SDK hands you the ringing call. It owns none of the experience — the ringing UI, ringtone, browser notifications and agent availability stay yours:

device.on('incomingCall', (call) => {
  // Your UI, your ringtone, your notifications.
  showRingingScreen({ from: call.remoteIdentity, to: call.calledNumber })

  answerButton.onclick = () => call.answer()
  rejectButton.onclick = () => call.reject()

  call.on('accepted', () => showInCallScreen())
  call.on('ended', (e) => hideCallUi(e.cause))
  call.on('failed', (e) => hideCallUi(e.cause))
})
  • call.answer() accepts the call, call.answer({ inputDeviceId }) pins a specific microphone, call.reject() refuses it with 486 Busy Here, and call.hangup() ends a call already in progress. All three are safe to call twice — a repeat is ignored rather than throwing.
  • call.calledNumber is the DID the caller dialled; call.remoteIdentity is the caller.

Both ended and failed are terminal. A rejected or cancelled call arrives as failed (cause Rejected, Canceled, …) and never as ended. Code that tears its UI down only on ended leaves the ringing screen up forever.

If nothing is subscribed to incomingCall, inbound calls are refused with 486 Busy Here. For <dl-dialer>, enable incoming-calls or assign inboundCallHandler to receive calls.

Worth knowing before shipping inbound:

  • The tab must stay open and SIP-registered. There is no push wake-up for browser sessions — a closed tab means the caller is not reached at all.
  • Microphone permission must already be granted on the origin. answer() requests the microphone, so a first-time prompt lands mid-call.
  • Every registered browser rings at once; the first to answer wins and the others see the call end.

Call history

Since 1.6.0.

fetchCallHistory sends the history command through your proxy and pages with its cursor:

import { CommandError, fetchCallHistory } from '@didlogic/voice-sdk'

let before: string | null = null
do {
  const page = await fetchCallHistory('/webrtc', { type: 'all', limit: 25, before })
  render(page.calls)
  before = page.nextBefore
} while (before && userWantsMore())

It takes { type, limit, before, number, from, to, signal, fetch }, like fetchCallerIds. A failure rejects with a CommandError carrying status (absent on a network failure), code and details from the error object (absent on a refusal from before the command ran), and the message. CommandError is the shared failure of every proxy command; CallerIdsError extends it. An abort passes through as the browser's AbortError.

Audio preprocessing

Since 1.2.0.

DeviceOptions accepts an optional audioProcessing block that controls the browser's native audio pipeline on the captured microphone track. It applies to both outbound and inbound calls:

const device = new Device({
  audioProcessing: {
    echoCancellation: true,
    noiseSuppression: true,
    autoGainControl: true,
    channelCount: 1, // mono — roughly halves the upstream audio bandwidth
  },
})

All four fields are optional; anything you omit is left to the browser.

Limitations, worth knowing before you rely on this:

  • echoCancellation, noiseSuppression and autoGainControl are already true by default in Chromium and Firefox. Setting them true therefore changes nothing — the real use of this option is to disable one (noiseSuppression: false when you route music or run your own DSP) or to request mono via channelCount: 1.
  • These are constraints the browser applies to getUserMedia; support and exact behaviour vary by browser and OS, and a browser may ignore a field it does not implement. Safari in particular honours fewer of them.
  • It is a device-wide setting, read when a call's media is set up. There is no per-call override.

CallCredentialsManager on its own:

  • caches the current credentials and refreshes them automatically refreshBeforeExpirySec (default 60) before expires_in;
  • retries refresh failures with exponential backoff (cap 5 min, up to 10 attempts);
  • supports pauseRefresh() / resumeRefresh() (the widget pauses while a call is active so a re-register does not drop the line).

Full export list

Runtime classes:

| Export | Since | Purpose | | -------------------------------------------------------------------------------- | ----- | -------------------------------------------------------------------------------- | | Device, VoiceCall, AudioManager | 1.0.0 | The low-level SIP/WebRTC stack (re-exported from @didlogic/voice-core) | | CallCredentialsManager | 1.0.0 | Proxy-backed authentication | | Dialer | 1.0.0 | The widget class itself, in case you want to subclass it | | CallHistory | 1.6.0 | The <dl-call-history> widget class | | Dialpad, Keypad, CallScreen, CallControls, CallerInfo, SettingsPanel | 1.0.0 | Screen classes | | AppBar, Select, IconButton, BottomSheet | 1.0.0 | Reusable components | | Header | 1.6.0 | The <dl-header> bar shared by <dl-dialer> and <dl-call-history> | | ClickToCallScanner | 1.0.0 | Programmatic page scanning | | fetchCallerIds(proxyUrl, options?), CallerIdsError, CallerIdChoice | 1.5.0 | The caller_ids command, its typed failure, the remembered choice per proxy URL | | fetchCallHistory(proxyUrl, options?), CommandError | 1.6.0 | The history command, and the typed failure shared by every proxy command |

Type-only:

| Since | Types | | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 1.0.0 | DeviceOptions, DeviceCredentials, AudioDeviceInfo, DeviceEventMap, CallEventMap, CallCredentials, CallCredentialsError, CallCredentialsManagerOptions, CallStatus, SelectOption, IconButtonVariant, ClickToCallOptions | | 1.1.0 | VoiceCallOptions, InboundCallHandler | | 1.2.0 | AudioProcessingOptions | | 1.3.0 | WebRtcSessionGrant, WebRtcSessionProvider | | 1.4.0 | DlDialerProps, DlDialerJSX — the <dl-dialer> JSX prop types, for your own JSX runtime augmentation | | 1.5.0 | CallerId, FetchCallerIdsOptions | | 1.6.0 | CallHistoryEntry, CallHistoryPage, CallHistoryType, CommandErrorDetail, FetchCallHistoryOptions, CallRequestDetail, DlCallHistoryProps, DlCallHistoryJSX |

Click-to-Call

The click-to-call attribute turns on host-page scanning and intercepts clicks on tel: links and elements carrying data-phone / data-tel / data-number:

<dl-dialer
  proxy-url="https://your-backend.example.com/webrtc"
  click-to-call
  click-to-call-selector=".article-body"
></dl-dialer>

<a href="tel:+15551234567">Call us</a>
<span data-phone="+15551234567">+1 555 123-4567</span>

The scanner:

  • catches tel: hrefs and elements with data-phone | data-tel | data-number;
  • finds bare phone numbers in text nodes via a regex;
  • uses MutationObserver — works in SPAs without page reloads;
  • injects a <style id="dl-click-to-call-styles"> with a CSS class .dl-click-to-call (with a hover state). Colors are configurable via --dl-click-to-call-color / --dl-click-to-call-hover-color.

For programmatic use, see ClickToCallScanner in the Headless API.

Browser support

Modern evergreen browsers with WebRTC and Web Components:

  • Chrome / Edge ≥ 100
  • Firefox ≥ 100
  • Safari ≥ 15

WebRTC requires HTTPS (or localhost). Microphone access is requested on the first call.

Versioning

Semantic Versioning. Breaking changes ship in major releases only. The documented attributes, events and CSS variables are the public API; anything not described here is internal and may change at any time.

License

MIT — see LICENSE.