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.
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
DeviceandVoiceCallclasses 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}
- The widget asks your backend for credentials.
- Your backend adds the secret SDK token and forwards the request to didlogic.
- didlogic answers with short-lived SIP credentials (valid for one hour).
- The widget registers over WebSocket and places or receives calls. It renews the credentials before they expire.
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 (
localhostis 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
Devicestarted 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:
- Accept a
POSTwith a JSON body from the widget. - Add an
api_tokenfield with your SDK token. - Forward the body to
https://app.didlogic.com/mobile/api/sdk/command. - 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:
| Command | Sent by | When | Since |
|---|---|---|---|
call | the widget | On start, and again about a minute before the credentials expire | 1.0.0 |
caller_ids | the widget | Once credentials are received, to fill the caller ID selector | 1.5.0 |
history | <dl-call-history>, or your code (fetchCallHistory) | When your page shows call history — see Call history | 1.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
}
| Field | Meaning |
|---|---|
session_id | SIP username for this session, 32 hex characters |
session_pwd | SIP password for this session |
wss_url | WebSocket URL to register on. Use it as returned — didlogic chooses it, so never hardcode a host |
expires_in | Seconds 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
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:
callandcaller_idsfor the widget, plushistoryif 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 asagent_uidto 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
| Attribute | Type | Default | Since | Description |
|---|---|---|---|---|
proxy-url | string | — | 1.0.0 | Required. URL of your proxy endpoint |
brand-name | string | Phone | 1.0.0 | Title in the widget header |
number | string | — | 1.0.1 | Number to prefill in the dialpad |
click-to-call | boolean | off | 1.0.0 | Make phone numbers on the page clickable — see Click-to-call |
click-to-call-selector | string | whole page | 1.0.0 | CSS selector limiting where click-to-call looks |
incoming-calls | boolean | off | 1.4.0 | Show the built-in incoming call screen — see Receiving calls |
caller-id | string | — | 1.5.0 | Number to present on outgoing calls — see Caller ID |
JavaScript properties
Functions cannot be passed as HTML attributes. Assign these on the element:
| Property | Type | Since | Description |
|---|---|---|---|
inboundCallHandler | (call: VoiceCall) => void or null | 1.1.0 | Handle incoming calls in your own UI |
Methods
| Method | Since | Description |
|---|---|---|
dial(number) | 1.6.0 | Place 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())
}
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}
| Request | Success | Body |
|---|---|---|
GET | 200 | { "inbound_routes": [{ "id", "number", "agent_uid", "active" }] } |
POST | 201 | { "id", "number", "agent_uid" } |
DELETE | 200 | { "id" } |
- Omit
agent_uidand 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
422with anerrormessage:Number not found(not your number),Route already exists(same number and agent),Route not found(an id of another token), or theagent_uidformat 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.
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-idattribute 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>
| Attribute | Description |
|---|---|
proxy-url | Required. Your proxy endpoint, the same as the dialer's |
dialer | The 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" }
| Field | Values | Default |
|---|---|---|
type | all, outbound, inbound, missed | all |
limit | 10, 25, 50, 100 | 25 |
before | the next_before of the previous page | the newest calls |
number | Digits to find in the caller, callee or DID; other characters are ignored; 1 to 20 digits | any number |
from, to | ISO 8601 date-times bounding when the call started; to not before from | all 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"
}
| Field | Meaning |
|---|---|
id | The 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 |
direction | inbound or outbound |
status | answered, missed (inbound, not answered) or not_connected (outbound, not connected) |
from, to | Caller and called number |
did_number | Your DID the call came in on, or null |
started_at | Start time, UTC |
duration | Seconds |
next_before | Pass 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" }
]
}
}
| Status | code | When | What to do |
|---|---|---|---|
422 | validation_failed | type, limit, before, number, from or to is invalid; details names each field | Fix the request |
409 | sip_account_missing | The token has no SIP account | Contact support |
503 | call_history_unavailable | Call history is temporarily unavailable | Retry 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;
}
| Variable | Default | Used for |
|---|---|---|
--dl-primary | #447bf2 | Accent, dialed digits |
--dl-success | #66d391 | Call button |
--dl-danger | #ed2643 | Hang-up button |
--dl-danger-hover | #d41e38 | Hang-up button, hover |
--dl-bg | #ffffff | Widget background |
--dl-surface | #f0f0f0 | Inner surfaces |
--dl-surface-hover | #e4e4e4 | Inner surfaces, hover |
--dl-text | #121011 | Primary text |
--dl-text-secondary | #9298a3 | Secondary text |
--dl-border | #e7eaf0 | Borders and separators |
--dl-font-family | KlavikaBasicRegular, Arial, sans-serif | UI font |
--dl-font-mono | PTMonoRegular, Arial, serif | Digits |
--dl-radius | 6px | Corner radius |
--dl-radius-round | 50% | Round buttons |
--dl-width | 360px | Widget width |
--dl-height | 680px | Widget height |
--dl-sheet-height | 100% | 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
| Member | Description |
|---|---|
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 |
callerId | Caller ID for the next calls (since 1.5.0) |
setInputDevice(id) / setOutputDevice(id) | Switch microphone or speaker, including during a call |
audio | AudioManager: list microphones and speakers |
unregister() / destroy() | Stop receiving calls / release everything |
on(event, handler) / off(event, handler) | Subscribe / unsubscribe |
| Event | Payload | Meaning |
|---|---|---|
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 |
call | VoiceCall | An outgoing call was started |
incomingCall | VoiceCall | A call is ringing (since 1.1.0). With no listener, it is refused with 486 |
VoiceCall
| Member | Description |
|---|---|
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' |
remoteIdentity | The other party's number |
calledNumber | For incoming calls, the DID that was dialed (since 1.1.0) |
muted, held, duration, terminated | Current state |
answer(), reject() and hangup() are safe to call twice; the second call is ignored.
| Event | Payload | Meaning |
|---|---|---|
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,
},
})
| Option | Since | Description |
|---|---|---|
audioProcessing | 1.2.0 | Browser audio processing for the microphone, for outgoing and incoming calls |
iceServers | 1.0.0 | Your own STUN/TURN servers |
registrationExpiry | 1.0.0 | SIP 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.
| Option | Default | Description |
|---|---|---|
proxyUrl | — | Required. Your proxy endpoint |
refreshBeforeExpirySec | 60 | Renew this many seconds before expiry |
retryDelayMs | 10000 | First retry delay when a renewal fails; doubles on each attempt |
maxRetryDelayMs | 300000 | Cap on the retry delay |
maxRetryAttempts | 10 | Renewal 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.
| Status | error | Cause | What to do |
|---|---|---|---|
401 | Invalid API token | The token is wrong, or was deleted | Check the token in your backend secrets |
403 | Something went wrong. Please contact [email protected] | The account is inactive or blocked, for example by a negative balance | Top up, or contact support |
404 | Unknown command | The proxy sent a command didlogic does not know | Fix the proxy |
422 | SDK token has no SIP account | The token's SIP account is missing | Contact support |
422 | agent_uid must be 1 to 32 letters, digits, underscores or hyphens | Invalid agent_uid from your proxy | Fix the value |
429 | Rate limit exceeded | See Limits | Back off and retry |
Limits
| Limit | Applies to |
|---|---|
| 60 requests per minute | Each token |
| 120 requests per minute | Each 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
| Symptom | Likely cause |
|---|---|
Status line: Missing proxy-url attribute or sessionProvider property | proxy-url is not set |
Status line shows a network error, such as Failed to fetch | The 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 settings | The user blocked the microphone for your site |
| Ready, but incoming calls never ring | The 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 audio | A network blocking WebRTC media; pass your own TURN server in iceServers |
| Outgoing calls show an unexpected caller ID | The 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
| Version | Released | Adds |
|---|---|---|
| 1.6.0 | 2026-10-08 | Call history: <dl-call-history> widget, history command with number and date filters, fetchCallHistory, CommandError, Dialer.dial |
| 1.5.0 | 2026-09-16 | Caller ID: caller_ids command, caller-id attribute, Settings selector, fetchCallerIds, Device.callerId |
| 1.4.0 | 2026-09-06 | Built-in incoming call screen (incoming-calls); visible connection loss and registration retry |
| 1.3.0 | 2026-09-05 | Internal improvements. No changes needed in your integration |
| 1.2.0 | 2026-07-23 | audioProcessing device option |
| 1.1.0 | 2026-07-21 | Incoming calls: incomingCall, answer() / reject(), calledNumber, inboundCallHandler |
| 1.0.1 | 2026-05-28 | number attribute |
| 1.0.0 | 2026-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.