Skip to main content

Voice SDK

The Voice SDK puts a phone into your web app. Your users and agents place and receive calls straight from the browser, with no desk phones, softphone installs or SIP setup on their side. The numbers, the SIP infrastructure and the billing stay on the didlogic side, under your account.

What you can build​

  • Calling inside your CRM or helpdesk. Agents call leads and customers from the record they are looking at, see past calls next to it, and phone numbers on the page become clickable.
  • A support line that rings in the browser. Route a number to your app and answer calls in the tab your team already works in. Ring everyone, or only the agent who should take the call.
  • Calling built into your SaaS product. Give your own customers a phone under your brand: restyle the widget with CSS, or build your own UI on the headless API.
tip

The SDK is for calls that happen in a browser tab, with a person at the screen. When calls should start from your server instead, use the Click2Call API: one POST request rings your agent's SIP phone or mobile app first, then connects them to the customer, with no widget on the page. It's the better fit for automated workflows, for teams on desk phones or softphones, and for AI agents placing outbound calls.

What's included​

  • Outgoing and incoming calls
  • Caller ID selection from your didlogic numbers
  • Mute, DTMF keypad, microphone and speaker selection during a call
  • Call history with search, date filters and one-click callback
  • Click-to-call: phone numbers on your page become call buttons
  • Routing of incoming calls to specific agents
  • Theming with CSS variables
  • A headless API for a fully custom UI

Setup takes three steps: create an SDK token in the portal, add a small proxy endpoint to your backend, and drop the widget on your page.

Widget or headless​

The SDK ships as one npm package, @didlogic/voice-sdk, with two ways in:

  • The widgets — a <dl-dialer> web component with a dialpad, call screen, device settings, incoming-call screen and caller ID selector, and a <dl-call-history> call log (since 1.6.0). One tag each, any framework.
  • The headless API — the Device and VoiceCall classes underneath the widget, for when you draw the UI yourself.

This page describes version 1.6.0. Every feature added after 1.0.0 is marked Since 1.x.0; check what you have installed with npm ls @didlogic/voice-sdk.

How it works​

┌──────────┐   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}
  1. The widget asks your backend for credentials.
  2. Your backend adds the secret SDK token and forwards the request to didlogic.
  3. didlogic answers with short-lived SIP credentials (valid for one hour).
  4. The widget registers over WebSocket and places or receives calls. It renews the credentials before they expire.
warning

The one rule that shapes everything: the SDK token lives on your server only. The browser never sees it. Anyone holding the token can place calls billed to your account.

Before you begin​

You need:

  • A didlogic account that is active, not blocked and has funds. A blocked account is refused with 403.
  • At least one DID (a phone number bought on didlogic). It is the caller ID your outgoing calls present, and the number customers call to reach you.
  • A backend you control, to hold the token and run a small proxy endpoint.
  • HTTPS on the page hosting the widget. Browsers allow microphone access and WebRTC only on secure origins (localhost is the exception for development).

Step 1 — Create an SDK token​

In the didlogic portal open API & Webhooks, find SDK Tokens and create a token. The value is shown once: copy it straight into your backend's secrets — an environment variable or a vault, never the frontend and never git.

What the token is:

  • One token belongs to one didlogic user. Calls are placed and billed as that user.
  • Each token gets its own SIP account, created automatically together with it and shown in the portal as SDK Instance: your token name. Every widget or Device started with that token registers as this account.
  • Deleting a token deletes its SIP account. The portal refuses to delete a token while numbers still route to its SIP account — remove those routes first.

Create one token per application or environment (for example "Website production" and "Website staging"), so that each can be routed and removed on its own.

Step 2 — Add the proxy endpoint​

The proxy is one HTTP endpoint on your backend. It must:

  1. Accept a POST with a JSON body from the widget.
  2. Add an api_token field with your SDK token.
  3. Forward the body to https://app.didlogic.com/mobile/api/sdk/command.
  4. Return didlogic's status code and body unchanged. The widget reads both to tell the user what went wrong.

What the proxy forwards​

Three commands go to the same URL. The widget sends the first two on its own:

CommandSent byWhenSince
callthe widgetOn start, and again about a minute before the credentials expire1.0.0
caller_idsthe widgetOnce credentials are received, to fill the caller ID selector1.5.0
history<dl-call-history>, or your code (fetchCallHistory)When your page shows call history — see Call history1.6.0
POST /webrtc HTTP/1.1
Content-Type: application/json

{ "command": "call" }

Your proxy forwards:

POST /mobile/api/sdk/command HTTP/1.1
Host: app.didlogic.com
Content-Type: application/json

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

The token can also be sent in the X-Sdk-Api-Token header instead of the body.

The call response​

{
"session_id": "8f14e45fceea167a5a36dedd4bea2543",
"session_pwd": "c9f0f895fb98ab9159f51fd0297e236d",
"wss_url": "wss://…",
"expires_in": 3600
}
FieldMeaning
session_idSIP username for this session, 32 hex characters
session_pwdSIP password for this session
wss_urlWebSocket URL to register on. Use it as returned — didlogic chooses it, so never hardcode a host
expires_inSeconds the credentials stay valid (3600)

Every call to call issues a new pair. Nothing has to be stored on your side.

The caller_ids response​

Since 1.5.0. The numbers this token's account may present as caller ID — the DIDs you bought on didlogic:

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

If your proxy allow-lists commands, admit caller_ids too. When it is refused, the widget still works and simply shows no caller ID selector.

Reference proxy (Node.js 18+, no dependencies)​

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

const PORT = process.env.PORT ?? 3000
const SDK_TOKEN = process.env.SDK_TOKEN
const UPSTREAM = 'https://app.didlogic.com/mobile/api/sdk/command'
const ALLOW_ORIGIN = process.env.ALLOW_ORIGIN // e.g. https://www.example.com
const ALLOWED_COMMANDS = new Set(['call', 'caller_ids', 'history'])
const HISTORY_FIELDS = ['type', 'limit', 'before', 'number', 'from', 'to']

if (!SDK_TOKEN || !ALLOW_ORIGIN) {
console.error('SDK_TOKEN and ALLOW_ORIGIN are required')
process.exit(1)
}

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

const reply = (res, status, body) =>
res.writeHead(status, { 'Content-Type': 'application/json', ...cors }).end(JSON.stringify(body))

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

// Authenticate your own user here (session cookie, JWT) before going further.

const chunks = []
for await (const chunk of req) chunks.push(chunk)
let body
try {
body = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}')
} catch {
return reply(res, 400, { error: 'Invalid JSON' })
}
if (!ALLOWED_COMMANDS.has(body.command)) return reply(res, 404, { error: 'Unknown command' })

const upstream = await fetch(UPSTREAM, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
command: body.command,
...(body.command === 'history'
? Object.fromEntries(HISTORY_FIELDS.filter((key) => key in body).map((key) => [key, body[key]]))
: {}),
api_token: SDK_TOKEN,
}),
})

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

Run it:

SDK_TOKEN=<your token> ALLOW_ORIGIN=https://www.example.com node proxy.mjs
Never put the SDK token in the browser

Not in an HTML attribute, not in a JavaScript bundle, not in a frontend .env file. A token that reaches client code can be used by anyone to place calls on your account. If it leaks, delete it in the portal and create a new one.

Securing the proxy​

The reference proxy forwards for anyone who can reach it. In production:

  • Authenticate your users with your existing session or JWT, so only logged-in users get SIP credentials. An open proxy is an open phone line on your bill.
  • Restrict CORS to the origin that hosts the widget, as the reference does.
  • Forward only the commands you use: call and caller_ids for the widget, plus history if your page shows history. The reference proxy rebuilds the body instead of spreading what the browser sent, so a browser cannot smuggle extra fields such as agent_uid to didlogic.
  • Rate-limit per user if needed. didlogic's own limits are per token and per IP (see Limits), and your proxy shares one IP across all your users.

Step 3 — Add the widget​

Install​

npm install @didlogic/voice-sdk

Plain JavaScript or any framework​

import '@didlogic/voice-sdk'

The import registers the custom elements. Then use the tag anywhere in your markup:

<dl-dialer proxy-url="https://api.example.com/webrtc" brand-name="Acme Phone"></dl-dialer>

Without a bundler​

The package ships a UMD build that registers the same elements:

<script src="https://unpkg.com/@didlogic/[email protected]/dist/dialer.umd.js"></script>

<dl-dialer proxy-url="https://api.example.com/webrtc" brand-name="Acme Phone"></dl-dialer>

Pin the version in the URL so an SDK release never changes your page unannounced.

React​

import '@didlogic/voice-sdk'

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

The package declares <dl-dialer> for TypeScript in React 18 and React 19, so it type-checks with no extra declaration. For other JSX runtimes (Preact, Solid, Vue JSX) declare it yourself with the exported DlDialerProps type (since 1.4.0).

Check that it works​

Open the page. The status line at the top of the widget goes through Connecting... and Registering SIP... to Ready with a green dot. Dial a number and press call; the browser asks for microphone access on the first call.

If it stops on an error instead, see Errors and limits.

Widget reference​

Attributes​

AttributeTypeDefaultSinceDescription
proxy-urlstring—1.0.0Required. URL of your proxy endpoint
brand-namestringPhone1.0.0Title in the widget header
numberstring—1.0.1Number to prefill in the dialpad
click-to-callbooleanoff1.0.0Make phone numbers on the page clickable — see Click-to-call
click-to-call-selectorstringwhole page1.0.0CSS selector limiting where click-to-call looks
incoming-callsbooleanoff1.4.0Show the built-in incoming call screen — see Receiving calls
caller-idstring—1.5.0Number to present on outgoing calls — see Caller ID

JavaScript properties​

Functions cannot be passed as HTML attributes. Assign these on the element:

PropertyTypeSinceDescription
inboundCallHandler(call: VoiceCall) => void or null1.1.0Handle incoming calls in your own UI

Methods​

MethodSinceDescription
dial(number)1.6.0Place a call from your own code, as if the user dialed it

Events​

The widget emits no DOM events: it owns its UI and its call state. If your page needs to react to registration or call state, build on the headless API instead.

What the widget can do​

  • One call at a time. A click-to-call while a call is up is ignored.
  • During a call: mute, DTMF keypad, hang up, call duration.
  • Settings: microphone and speaker selection, caller ID (since 1.5.0).
  • The widget pauses credential renewal while a call is up, so a renewal never drops the line.

Receiving calls​

Since 1.1.0 (built-in screen since 1.4.0).

Incoming calls need two things: a number routed to the token, and a widget that agrees to take them.

1. Route a number to the token​

In the portal, open the number's destinations, add one of type SIP account and pick SDK Instance: your token name. After saving it shows as SDK.

  • A Webphone destination rings the didlogic web phone, not your widget.
  • A destination pointing at another token's SIP account rings only widgets started with that token.

You can also route numbers from your backend, and route them to specific users — see Routing calls to specific agents.

2. Turn on incoming calls in the widget​

Until you do, the widget refuses every incoming call with 486 Busy Here. There are two ways:

The built-in screen (since 1.4.0) shows the caller with Answer and Reject, then the normal call screen:

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

Your own UI (since 1.1.0) — assign inboundCallHandler, which receives the ringing VoiceCall. It takes priority over incoming-calls:

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

dialer.inboundCallHandler = (call) => {
showRinging({ from: call.remoteIdentity, to: call.calledNumber })

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

call.on('accepted', () => showInCall())
call.on('ended', () => hideCallUi())
call.on('failed', () => hideCallUi())
}
Both "ended" and "failed" end the call

A call the caller cancels, or that you reject, arrives as failed and never as ended. UI that only listens for ended stays on the ringing screen forever.

In React 19, pass the handler as a prop: <dl-dialer inboundCallHandler={handleCall} … />. React 18 stringifies every prop on a custom element, so the function never arrives — assign it through 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://api.example.com/webrtc" />

The effect deliberately has no dependency array: it reassigns the handler on every render, so the handler never sees stale state.

Before you rely on incoming calls​

  • The tab must be open and registered. There is no push wake-up for browsers: when no tab is open, the caller does not reach anyone.
  • Every registered browser rings at once. The first to answer takes the call; the others see it end. To ring only one person, see the next section.
  • Grant microphone access in advance. Answering asks for the microphone; a first-time permission prompt then lands mid-call.

Routing calls to specific agents​

By default every session started with a token rings for every number routed to it. To ring one user only — one support agent, one salesperson — name the user on both sides.

Name the agent on the session​

Your proxy adds agent_uid when it forwards call:

{ "command": "call", "api_token": "<SDK_TOKEN>", "agent_uid": "operator-7" }

agent_uid is your own identifier for the logged-in user: 1 to 32 letters, digits, underscores or hyphens. Anything else is refused with 422, not trimmed. The widget never sends it — your backend knows who is logged in, the browser does not.

Name the agent on the route​

Create routes from your backend with the routes API. It lives on its own path, takes the token in the X-Sdk-Api-Token header, and is not a proxy command:

GET    https://app.didlogic.com/mobile/api/sdk/routes
POST https://app.didlogic.com/mobile/api/sdk/routes { "number": "15551234567", "agent_uid": "operator-7" }
DELETE https://app.didlogic.com/mobile/api/sdk/routes/{id}
RequestSuccessBody
GET200{ "inbound_routes": [{ "id", "number", "agent_uid", "active" }] }
POST201{ "id", "number", "agent_uid" }
DELETE200{ "id" }
  • Omit agent_uid and the route rings the sessions that named no agent.
  • Routes add up. A new route joins the number's existing destinations, so one number can ring several agents; each route is removed on its own.
  • Refusals are 422 with an error message: Number not found (not your number), Route already exists (same number and agent), Route not found (an id of another token), or the agent_uid format error.

The pairing is exact in both directions: a session with an agent receives only routes naming that agent, and a session without one receives only routes naming none. A number routed to operator-7 does not ring anyone until operator-7 has a session open.

Keep the routes API off your proxy

Your proxy forwards what the browser asks for. Never route the browser to /mobile/api/sdk/routes: anyone with devtools could then re-route your numbers. Call it from your backend only.

Caller ID​

Since 1.5.0.

The widget's Settings tab lists the numbers from the caller_ids command. The chosen number is presented on every outgoing call after that. Any of the account leaves the choice to didlogic.

  • The choice is remembered in the browser's localStorage, per proxy URL, so it survives reloads.
  • The caller-id attribute overrides the remembered choice — use it when your app decides the caller ID, for example per team.
  • A number you do not own is never presented. didlogic replaces it with one of your account's numbers.

Without the widget, use fetchCallerIds and Device.callerId:

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

try {
const ids = await fetchCallerIds('https://api.example.com/webrtc')
device.callerId = ids[0].callerid
} catch (err) {
if (err instanceof CallerIdsError) console.error(err.status, err.message)
}

fetchCallerIds takes an optional second argument, { signal, fetch }, to pass an AbortSignal or your own fetch (for example one that adds your auth headers). device.callerId keeps only the digits of what you assign; null clears it.

Call history​

Since 1.6.0.

The history command returns the calls of the token's SIP account, newest first, one page at a time. Show it with the ready-made widget, or in your own UI.

The history widget​

<dl-call-history> lists the calls with All, Outbound, Inbound and Missed tabs, a number search, a date range and a Load more button, themed like <dl-dialer>. Link it to a dialer by id, and a click on a call dials the other party back:

<dl-dialer id="phone" proxy-url="https://api.example.com/webrtc"></dl-dialer>
<dl-call-history proxy-url="https://api.example.com/webrtc" dialer="phone"></dl-call-history>
AttributeDescription
proxy-urlRequired. Your proxy endpoint, the same as the dialer's
dialerThe id of a <dl-dialer> to call back through; optional

A click on a call also dispatches a dl-call-request event with detail.number, so you can handle it yourself. You can also place a call from your own code with the dialer's dial(number) method. Call refresh() on the history widget to reload it, for example after a call ends.

In your own UI​

Use fetchCallHistory:

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

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

fetchCallHistory takes { type, limit, before, number, from, to, signal, fetch }: pass an AbortSignal or your own fetch as with fetchCallerIds. A failure rejects with a CommandError carrying status (absent on a network failure), the error code and its details; an abort passes through as the browser's AbortError.

The request​

{ "command": "history", "type": "all", "limit": 25, "before": "<next_before from the previous page>",
"number": "4420", "from": "2026-10-01T00:00:00Z", "to": "2026-10-07T23:59:59Z" }
FieldValuesDefault
typeall, outbound, inbound, missedall
limit10, 25, 50, 10025
beforethe next_before of the previous pagethe newest calls
numberDigits to find in the caller, callee or DID; other characters are ignored; 1 to 20 digitsany number
from, toISO 8601 date-times bounding when the call started; to not before fromall time

outbound lists only outgoing calls that connected, and inbound only incoming calls that were answered. Unanswered incoming calls are under missed; outgoing calls that did not connect appear only under all, with status not_connected. all holds every call.

The response​

{
"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": "eyJ0cyI6MTc5MTM2ODIwMCwiY2FsbGlkIjoiOGYxNGU0NWZjZWVhMTY3YTVhMzZkZWRkNGJlYTI1NDNAMTAuMC4wLjEifQ"
}
FieldMeaning
idThe call's ID, the same callid your webhooks carry: encrypted for an inbound call, plain for an outbound one. null when it cannot be given
directioninbound or outbound
statusanswered, missed (inbound, not answered) or not_connected (outbound, not connected)
from, toCaller and called number
did_numberYour DID the call came in on, or null
started_atStart time, UTC
durationSeconds
next_beforePass it as before to get the next page; null on the last page

next_before is opaque: pass it back unchanged. Paging walks the whole history without repeats.

Errors​

history answers errors in didlogic's public API format: one error object with a machine-readable code. Branch on code; message is for people.

{
"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" }
]
}
}
StatuscodeWhenWhat to do
422validation_failedtype, limit, before, number, from or to is invalid; details names each fieldFix the request
409sip_account_missingThe token has no SIP accountContact support
503call_history_unavailableCall history is temporarily unavailableRetry later

The refusals that happen before any command runs — 401, 403 and 429 in Errors and limits — keep the { "error": "<message>" } shape every command shares.

Who sees history​

History belongs to the token, not to one agent: every session started with a token sees all of that token's calls, whatever agent_uid it named. If only some of your users may see history, let your proxy decide who may send history.

Click-to-call​

With click-to-call, the widget turns phone numbers on your page into call buttons. This is a widget feature and is separate from the Click2Call API, which starts calls from your server.

<dl-dialer
proxy-url="https://api.example.com/webrtc"
click-to-call
click-to-call-selector=".contact-list"
></dl-dialer>

<div class="contact-list">
<a href="tel:+15551234567">Call sales</a>
<span data-phone="+15551234567">+1 555 123 4567</span>
Support: +1 555 765 4321
</div>

It picks up tel: links, elements with data-phone, data-tel or data-number, and phone numbers written in plain text. Content added later (a single-page app rendering a new view) is picked up too. Clickable numbers get the dl-click-to-call class; set their colors with --dl-click-to-call-color and --dl-click-to-call-hover-color.

Limit the scan with click-to-call-selector: on a page full of order numbers and dates, a plain-text scan of the whole page can mistake one for a phone number.

Theming​

Set CSS custom properties on dl-dialer or any ancestor:

dl-dialer {
--dl-primary: #ff6b00;
--dl-success: #00aa44;
--dl-width: 320px;
--dl-height: 600px;
}
VariableDefaultUsed for
--dl-primary#447bf2Accent, dialed digits
--dl-success#66d391Call button
--dl-danger#ed2643Hang-up button
--dl-danger-hover#d41e38Hang-up button, hover
--dl-bg#ffffffWidget background
--dl-surface#f0f0f0Inner surfaces
--dl-surface-hover#e4e4e4Inner surfaces, hover
--dl-text#121011Primary text
--dl-text-secondary#9298a3Secondary text
--dl-border#e7eaf0Borders and separators
--dl-font-familyKlavikaBasicRegular, Arial, sans-serifUI font
--dl-font-monoPTMonoRegular, Arial, serifDigits
--dl-radius6pxCorner radius
--dl-radius-round50%Round buttons
--dl-width360pxWidget width
--dl-height680pxWidget height
--dl-sheet-height100%DTMF keypad sheet height

Headless API​

Use the classes under the widget when you draw the whole UI yourself.

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

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

credentials.onCredentials = (creds) =>
device.applyCredentials({
wssUrl: creds.wssUrl,
sipUser: creds.sipUser,
sipPassword: creds.sipPassword,
})
credentials.onError = (err) => console.error('credentials', err.status, err.message)

device.on('registered', async () => {
const call = await device.call('15551234567')
call.on('accepted', () => console.log('connected'))
call.on('ended', (e) => console.log('ended', e.cause))
call.on('failed', (e) => console.log('failed', e.cause, e.statusCode))
})
device.on('registrationFailed', (e) => console.error('registration failed', e.cause))

await credentials.fetch()

device.call() throws until the device is registered, so place calls after the registered event. Numbers are dialed as digits in international format, without + or spaces.

Device​

MemberDescription
new Device(options?)See Device options
applyCredentials({ wssUrl, sipUser, sipPassword })Register, or re-register with renewed credentials
call(number, inputDeviceId?)Place a call; resolves with a VoiceCall
getCurrentCall()The call in progress, or null
callerIdCaller ID for the next calls (since 1.5.0)
setInputDevice(id) / setOutputDevice(id)Switch microphone or speaker, including during a call
audioAudioManager: list microphones and speakers
unregister() / destroy()Stop receiving calls / release everything
on(event, handler) / off(event, handler)Subscribe / unsubscribe
EventPayloadMeaning
registered—Ready to place and receive calls
unregistered—No longer registered
registrationFailed{ cause }Registration refused or timed out
transportConnected / transportDisconnected—The WebSocket came up / dropped
callVoiceCallAn outgoing call was started
incomingCallVoiceCallA call is ringing (since 1.1.0). With no listener, it is refused with 486

VoiceCall​

MemberDescription
answer({ inputDeviceId? })Answer a ringing call (since 1.1.0)
reject()Refuse a ringing call with 486 Busy Here (since 1.1.0)
hangup()End the call
mute(muted?) / hold(held?)Toggle, or set explicitly
sendDTMF(tones)Send keypad tones, e.g. '5' or '1234#'
direction'inbound' or 'outbound'
remoteIdentityThe other party's number
calledNumberFor incoming calls, the DID that was dialed (since 1.1.0)
muted, held, duration, terminatedCurrent state

answer(), reject() and hangup() are safe to call twice; the second call is ignored.

EventPayloadMeaning
accepted—The call is connected
ended{ cause }A connected call ended
failed{ cause, statusCode, reasonPhrase }The call never connected: busy, rejected, canceled, unreachable
muted{ muted }Mute changed
held{ held }Hold changed

Device options​

const device = new Device({
audioProcessing: {
echoCancellation: true,
noiseSuppression: false,
autoGainControl: true,
channelCount: 1,
},
})
OptionSinceDescription
audioProcessing1.2.0Browser audio processing for the microphone, for outgoing and incoming calls
iceServers1.0.0Your own STUN/TURN servers
registrationExpiry1.0.0SIP registration lifetime, in seconds (default 300)

Echo cancellation, noise suppression and auto gain are already on by default in Chrome and Firefox, so the useful settings are turning one off (for example noiseSuppression: false for music on hold or your own DSP) and channelCount: 1 to send mono and roughly halve upstream bandwidth. Browsers may ignore settings they do not support; Safari supports fewer of them.

CallCredentialsManager​

Fetches credentials through your proxy and renews them before they expire.

OptionDefaultDescription
proxyUrl—Required. Your proxy endpoint
refreshBeforeExpirySec60Renew this many seconds before expiry
retryDelayMs10000First retry delay when a renewal fails; doubles on each attempt
maxRetryDelayMs300000Cap on the retry delay
maxRetryAttempts10Renewal attempts before giving up

Methods: fetch(), getCredentials(), pauseRefresh() / resumeRefresh() (pause renewals during a call), destroy().

Only renewals are retried. When the first fetch() fails, it rejects and calls onError; retrying is up to you. After the last failed renewal, onError receives permanent: true.

Errors and limits​

Errors​

Every error from didlogic is JSON. The commands the widget sends answer with an error message, as below; history uses an error object with a code (see Call history). The widget shows it in its status line as <status>: <message> with a Retry button.

StatuserrorCauseWhat to do
401Invalid API tokenThe token is wrong, or was deletedCheck the token in your backend secrets
403Something went wrong. Please contact [email protected]The account is inactive or blocked, for example by a negative balanceTop up, or contact support
404Unknown commandThe proxy sent a command didlogic does not knowFix the proxy
422SDK token has no SIP accountThe token's SIP account is missingContact support
422agent_uid must be 1 to 32 letters, digits, underscores or hyphensInvalid agent_uid from your proxyFix the value
429Rate limit exceededSee LimitsBack off and retry

Limits​

LimitApplies to
60 requests per minuteEach token
120 requests per minuteEach IP address calling didlogic

Both count every request — call, caller_ids, history and the routes API. A widget makes a handful of requests per hour, so the limits only bite when many users start at once: your proxy is one IP for all of them, and one token is shared by everyone using it. Spread large deployments over several tokens.

Troubleshooting​

SymptomLikely cause
Status line: Missing proxy-url attribute or sessionProvider propertyproxy-url is not set
Status line shows a network error, such as Failed to fetchThe proxy is unreachable, or CORS blocks it — check Access-Control-Allow-Origin
Stuck on Registering SIP..., or Failed: …The browser cannot reach wss_url: a firewall or proxy blocking WebSockets
Microphone access denied — allow it in browser settingsThe user blocked the microphone for your site
Ready, but incoming calls never ringThe number is not routed to SDK Instance: your token name, incoming-calls / inboundCallHandler is not set, or the agent_uid on the session and the route differ
Calls connect with no audioA network blocking WebRTC media; pass your own TURN server in iceServers
Outgoing calls show an unexpected caller IDThe selected number is not one of your DIDs, so didlogic picked one

Browser support​

Current browsers with WebRTC and Web Components:

  • Chrome and Edge 100 or later
  • Firefox 100 or later
  • Safari 15 or later

The page must be served over HTTPS (or localhost during development).

Versions​

VersionReleasedAdds
1.6.02026-10-08Call history: <dl-call-history> widget, history command with number and date filters, fetchCallHistory, CommandError, Dialer.dial
1.5.02026-09-16Caller ID: caller_ids command, caller-id attribute, Settings selector, fetchCallerIds, Device.callerId
1.4.02026-09-06Built-in incoming call screen (incoming-calls); visible connection loss and registration retry
1.3.02026-09-05Internal improvements. No changes needed in your integration
1.2.02026-07-23audioProcessing device option
1.1.02026-07-21Incoming calls: incomingCall, answer() / reject(), calledNumber, inboundCallHandler
1.0.12026-05-28number attribute
1.0.02026-05-07<dl-dialer> widget, Device / VoiceCall, CallCredentialsManager, click-to-call, theming

Before 1.1.0, incoming calls were refused with 405 Method Not Allowed. Versions follow semantic versioning: a minor release adds features and never breaks an existing integration.