Install @dialt/sdk from npm. To hear Dialt before integrating, open the playground.
Browser quickstart
1. Install the SDK
npm install @dialt/sdk
@trelis/converse with @dialt/sdk. The old npm package is deprecated and receives no further releases.2. Create a persistent key
Sign in to API & Billing and create a key. Keys begin with ck_ and are shown once. Store the key as a server-side secret; never put it in browser JavaScript.
3. Add a credential route to your backend
Your authenticated backend exchanges its persistent key for a short-lived credential bound to one browser session:
import { randomUUID } from 'node:crypto';
app.post('/voice/session', requireUser, async (_req, res) => {
const upstream = await fetch('https://dialt.com/api/v1/session-keys', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.DIALT_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ session_id: randomUUID() }),
});
res.status(upstream.status).json(await upstream.json());
});
Protect this route with your own user authentication. The browser should receive only the returned scoped credential.
4. Connect from the browser
import { DialtClient } from '@dialt/sdk';
const response = await fetch('/voice/session', {
method: 'POST', credentials: 'same-origin',
});
if (!response.ok) throw new Error(`Voice credential failed: ${response.status}`);
const credential = await response.json();
const client = new DialtClient({
url: 'wss://dialt.com/ws',
sessionId: credential.session_id,
apiKey: credential.api_key,
mode: { kind: 'dialt' },
});
client.addEventListener('utterance', ({ detail }) => {
console.log('Assistant:', detail.text);
});
client.addEventListener('error', ({ detail }) => {
console.error(detail.detail || detail.error);
});
client.addEventListener('silent_mic', ({ detail }) => {
showMicWarning(detail.reason, detail.peak);
});
startButton.addEventListener('click', async () => {
await client.unlockAudio(); // keep this inside the user gesture
await client.connect();
await client.startMic();
});
stopButton.addEventListener('click', () => client.close());
Each inbound JSON frame is emitted once under its typed event name and once under the catch-all event name. Choose one subscription style for a handler; subscribing the same renderer to both will display the same transcript twice.
A silent_mic event is a nonfatal warning that the server has received sustained digital silence or an unusually low startup signal. Keep the session live while prompting the user to check the selected input, hardware mute, and browser and operating-system permissions. The event includes reason, duration_ms, and the observed PCM16 peak.
Prepare the scoped credential before enabling the Start button. If it has sat unused for more than 10 minutes, mint a fresh one so the complete two-hour session window remains available.
Authentication
| Credential | Use | Where it belongs |
|---|---|---|
Persistent ck_… key | Create scoped browser credentials; connect trusted Python or WebSocket clients. | Backend or trusted service only. |
| Scoped session key | Authenticate connections for one bound session_id until the key expires. | May be returned to the browser. |
POST /api/v1/session-keys
Exchange a persistent key for a browser-safe session credential.
POST https://dialt.com/api/v1/session-keys
Authorization: Bearer ck_your_key
Content-Type: application/json
{"session_id":"your-unique-session-id"}
HTTP/1.1 201 Created
{
"api_key": "short-lived-session-key",
"session_id": "your-unique-session-id",
"expires_in": 7800
}
| Status | Meaning |
|---|---|
201 | Credential created. |
400 | Invalid or missing session_id. |
401 | Persistent key missing, unknown or revoked. |
415 | Request is not JSON. |
The scoped key expires after 130 minutes and works only with the returned session_id. It may be reused for sequential connections or reconnects carrying that ID until expiry; expiry also ends a connection still using it. Revoking the persistent key prevents new exchanges but does not invalidate scoped keys already issued.
session_id is 1–64 characters. The first character must be a letter, number, underscore or hyphen; later characters may also contain a period. In regex form: [A-Za-z0-9_-][A-Za-z0-9._-]{0,63}.
Browser SDK reference
DialtClient(options)
| Option | Type / default | Description |
|---|---|---|
url | string, required | Use wss://dialt.com/ws. |
sessionId | string, generated | Must match a scoped key's bound session ID. |
apiKey | string | Scoped browser key or, in trusted environments only, a persistent key. |
mode | {kind:'dialt'} | Conversation configuration; see Mode options below. |
user | string | Optional stable identifier for your user. It is metadata, not authentication. |
timezone | string | Optional IANA timezone such as Europe/Dublin. |
player | StreamingPlayer | Optional caller-supplied playback implementation. |
resumeState | object | null, null | Opaque state previously returned by exportResumeState(). Imports it before the first connection. |
autoReconnect | boolean, true | Reconnect after abnormal transport loss and resume the prior conversation while the server token remains valid. |
reconnectBaseMs | number, 500 | Initial reconnect delay. |
reconnectMaxMs | number, 5000 | Maximum reconnect delay. |
maxReconnectAttempts | number, 12 | Attempts before a terminal error. |
rawAssist | boolean, false | Support diagnostic for comparing processed and unprocessed microphone audio. Leave disabled unless Dialt support asks you to enable it. |
inputDeviceId | string | null, null | Preferred microphone device ID; null follows the system default. |
captureStartupTimeoutMs | number, 2000 | Bound for receiving the first worklet frame before the SDK treats capture as stalled. |
Resume after a page reload
The SDK automatically resumes an unexpected WebSocket loss while the JavaScript client remains alive. A reload destroys that client, so save its opaque state whenever resume_state fires and pass the saved value to the replacement client:
const storageKey = 'dialt-resume-state';
const clientOptions = {
url: 'wss://dialt.com/ws',
sessionId: credential.session_id,
apiKey: credential.api_key,
mode: originalMode, // reconstruct the same options used before reload
};
let resumeState = null;
try {
resumeState = JSON.parse(sessionStorage.getItem(storageKey) || 'null');
} catch {
sessionStorage.removeItem(storageKey);
}
const client = new DialtClient(clientOptions);
if (resumeState !== null) {
try {
client.importResumeState(resumeState);
} catch {
sessionStorage.removeItem(storageKey);
}
}
client.addEventListener('resume_state', ({ detail }) => {
if (detail.state) sessionStorage.setItem(storageKey, JSON.stringify(detail.state));
else sessionStorage.removeItem(storageKey);
});
Reconstruct the same client options on the replacement page. Explicit mode fields override stashed configuration, so falling back to constructor defaults can change the resumed session (for example, disabling an earlier web_search: true). The guarded load above also removes malformed or unsupported stored state instead of trapping the page in a reload failure.
resume_state carries detail.state, the same value as exportResumeState(). It fires when ready supplies or rotates a token and with null when the SDK invalidates one. Treat non-null state as a short-lived credential: keep it in tab-scoped sessionStorage, do not inspect or modify its fields, and never send it anywhere except back to Dialt through the SDK.
Import does not extend the server window. Resume state is short-lived (about 150 seconds), single-use, bound to the account that created it, and lost on a broker restart. If it is stale, connect() rejects and emits resume_failed; the SDK clears the state so the application can offer a deliberate fresh start. Calling close() also clears it because an intentional end must not resume later.
Dialt mode
mode: {
kind: 'dialt',
modality: 'voice', // 'voice' (default) or 'text'
voice: 'optional-voice-key',
instructions: 'Optional application instructions',
greeting: 'Hello!', // string, false, or omit for the default
web_search: false,
end_call: false, // let the agent end the session with the managed end_call(farewell) tool
flow: false,
background_audio: false, // webrtc transport only (server-mixed bed)
tools: [],
temperature: 0.7,
}
modality: 'text' keeps the same model, instructions, tools, history, greeting and conversation events without opening an audio pipeline. Use the WebSocket transport, omit microphone setup, and commit turns with client.sendText(text). Text sessions reject microphone and WebRTC audio.
web_search defaults to false. Dialt sessions may combine it with client tools; search joins the same automatic tool-selection loop as a managed lookup.
end_call defaults to false. When true, Dialt declares the managed end_call(farewell) tool to the agent. On the turn that calls it only the farewell is spoken, then session_end_requested arrives with that farewell, and the server closes after a short grace unless the user speaks. Without the flag the agent cannot end the session; the host ends it with requestWrapUp() or by disconnecting. While enabled, the name end_call is reserved, like web_search.
background_audio mixes a soft underscore into the assistant audio for the whole call, so the silence between turns feels connected. It requires transport: 'webrtc' (the bed rides the playout track, which is the only downlink that runs continuously between turns), and the server rejects the start frame if it is set on the WebSocket transport. The SDK drops it (with a console warning) rather than sending it whenever the session is not on WebRTC, including when WebKit downgrades the transport for you. On any transport, prefer the SDK's own ambience option below.
The constructor option ambience ('thinking' default; 'off', 'continuous', or an object { mode, afterS, fadeInS, fadeOutS, level }) plays the bed client-side on the WebSocket transport, mixed through the SDK player so it sits inside the echo canceller's far-end reference, including on iOS (over webrtc the local ambience stays silent; use background_audio). 'continuous' runs it under the whole call from the first reply; 'thinking' plays it only while Dialt is blocking on a tool result with nothing to say (the working event): it fades in after about 1.5 s and out again under the reply's first syllables, so a slow backend is heard as "still working" rather than dead air. client.setAmbience(mode) switches live.
Methods
| Method | Returns | Description |
|---|---|---|
unlockAudio() | Promise | Unlocks browser playback. Call inside the Start button's user gesture. |
connect(options?) | Promise<DialtClient> | Connects and resolves after ready. Options: temperature, noGreeting. |
exportResumeState() | object | null | Returns a fresh JSON-serializable snapshot of the current opaque resume state. |
injectContext(text, {messageId?, role?, reply?}) | Promise<ack> | Adds typed user text or silent host context and resolves with the broker's authoritative accepted/rejected acknowledgement. |
sendText(text) | boolean | Commits one typed user turn in modality: 'text'. The ordinary asr, turn, text_delta, utterance and done events follow, without audio. |
importResumeState(state) | void | Imports a state returned by exportResumeState(). Call only before connect(); null clears it. |
startMic({workletUrl?, sdkAec?, deviceId?}) | Promise<object> | Starts SDK-owned capture in voice mode and resolves only after a real worklet frame. Text mode rejects microphone capture. A stalled first capture is released and retried once; a second stall rejects with code: 'capture_stalled'. |
stopMic() | Promise | Releases SDK-owned microphone resources without closing the session. |
getInputDevices() | Promise<MediaDeviceInfo[]> | Enumerates available audio inputs. |
setInputDevice(deviceId) | Promise<object> | Selects and safely reacquires an input; pass null to follow the system default. |
setMicEnabled(enabled) | void | Temporarily gates SDK-owned microphone tracks. |
reset() | Promise | Clears playback and starts a fresh conversation on the same connection. |
setVoice(key) | void | Changes the voice beginning with the next assistant reply. |
close() | void | Stops capture and playback and closes the socket. |
closeAndWait(timeoutMs?) | Promise | Closes and waits for the WebSocket close handshake. |
pushMicFrame(frame) | void | Advanced: sends a caller-owned Float32 mono 16 kHz frame on an already-live session. |
appendAudio(frame) | Promise | Advanced: connects if needed, then sends one caller-owned Float32 frame. |
sendRawFrame(frame) | void | Support diagnostic: sends a synchronized unprocessed frame when rawAssist is enabled. |
sendAmbienceState(active) | void | Records whether your client-side ambience layer is active; it does not alter server audio. |
requestWrapUp(reason) | void | Asks the assistant to wrap up gracefully: it finishes any current reply, speaks a natural sign-off, then the server closes the session cleanly (1000 "idle"). Fire-and-forget; use a local stop as a backstop if the close never arrives. The Playground calls this at its three-minute microphone limit; SDK integrations do not inherit that UI limit. |
sendToolResult(id, content, { outcome, verified }) | void | Resolves a tool_call; only outcome: 'succeeded' with verified: true authorizes success narration. |
sendToolDeferred(id, { handle, statusLabel? }) | void | Releases an eligible long-running call from the voice turn while preserving its managed job identity. |
sendToolProgress(id, note) | void | Reports human-readable progress on an in-flight call; never resolves it. |
sendToolPartialResult(id, content, {reply, interaction}) | void | Delivers a structured segment of the eventual answer; reply: true asks Dialt to narrate it now, while interaction: {id?, prompt, options?} marks it as needing a user decision and is never silently dropped when the floor is busy. Omit interaction.id and the broker derives one, echoed in tool_job_narration.interaction_ids. |
sendToolInteractionUpdate(id, interactionId, state, {note?}) | Promise<ack> | Closes an open interaction (resolved/cancelled/superseded) without completing the call: pending or in-flight narration stops and the model is told not to act on it. Resolves with the deterministic tool_interaction_update_ack; applied: false carries a stable reason such as already_closed. |
setToolChoice(choice, {oneShot?}) | boolean | Restricts tool use mid-session ("auto"/"none"/"required"/{allowed}/{tool}); forced modes constrain the first planning round of the next user turns, oneShot reverts after one turn. Also available at session start as mode.tool_choice. |
sendToolCancel(id) | void | Cancels an in-flight tool call. |
narrationState(jobId) | string? | Last known tool_job_narration state (queued/started/superseded/cancelled/failed/resolved) for a queued interaction job. |
interactionState(interactionId) | string? | Last known narration state keyed by the stable interaction id. |
waitForNarrationState(jobId, states, {timeoutMs}) | Promise<string> | Resolves once jobId reaches one of states, or rejects on timeout. |
await injectContext(text, { messageId, role, reply }) resolves to {type: 'inject_context_ack', message_id, accepted, ...}. Typed final asr events echo message_id with input_source: 'text'; spoken input uses input_source: 'voice'. Rejections use accepted: false and may include retryable and detail.
Tool calls arrive as tool_call events (detail.id, detail.name, detail.args). Where the work runs is up to you: answer in the page, or forward the call to your backend and relay its result with sendToolResult.
Properties: client.mode is the active mode configuration; client.responding is true while an assistant reply is active.
One-shot helpers
sendFeedback({url, sessionId, rating?, text?, device?, browser?, apiKey?})sendClientError({url, sessionId?, detail, context?, apiKey?})
Both return promises and use a standalone WebSocket, so they can be called after the conversation socket has closed.
Advanced named exports are StreamingPlayer, MicCapture, EchoCanceller, needsSdkAec, audio conversion helpers, tagged-uplink helpers, and sample-rate/channel constants. Most applications should use DialtClient rather than assembling these pieces.
Microphone, playback and echo cancellation
startMic() requests mono audio with echo cancellation enabled and noise suppression and automatic gain control disabled. It resolves only after the worklet supplies an actual frame; an all-zero frame is valid silence. If no frame arrives within the bounded startup window, the SDK fully releases capture and reacquires once. A repeated stall emits failed and rejects with code: 'capture_stalled'.
Use warming_up, recovering, listening, and failed to render status. Do not add an application retry or a fallback timer that marks an opened-but-frame-less microphone ready.
getInputDevices() enumerates audio inputs and setInputDevice(deviceId) safely restarts active capture with that input. While capture is active, the SDK handles devicechange, emits devices_changed, follows system-default changes, and falls back to the default if an explicitly selected input disappears.
If you use pushMicFrame(), the SDK no longer owns capture. Supply echo-cancelled Float32 mono audio at 16 kHz. If assistant audio is audible through speakers and echo is not removed, the service may transcribe or react to its own voice.
Browser support
This is one Browser SDK, not a Chrome-only SDK. The table describes API compatibility targets, not production certification; applications use the same @dialt/sdk package in each. See the WebSocket platform table for the current production-validation status.
| Platform | API compatibility targets | Playback / capture path |
|---|---|---|
| macOS | Chrome, Brave, Firefox, Safari | Native browser AEC except Safari, which uses the SDK's WebKit path. |
| Windows | Chrome, Brave, Edge, Firefox | Native browser AEC. |
| Android | Chrome, Brave, Firefox | Native browser AEC; device audio policy remains browser/OS-controlled. |
| iPhone / iPad | Safari and current Chrome/Brave builds | SDK WebKit-compatible AEC and WebSocket transport. |
Automated Chromium/WebKit/Firefox coverage checks APIs and lifecycle. It does not certify every branded browser or operating-system combination. Acoustic echo cancellation, maximum loudness and wired/Bluetooth routing require physical-device validation. iOS browser builds using an alternative engine become production-supported only after that engine/device combination is certified.
The SDK plays assistant audio at unity gain and does not add a limiter, boost output, select a physical speaker, or manipulate navigator.audioSession. Device volume and routing remain browser/OS policy, and mobile browsers may attenuate playback while microphone capture is active. Keeping capture active preserves barge-in; guaranteed speaker routing requires a native media integration.
WebRTC transport (experimental)
Experimental: the API is stable, but this transport is newly shipped and still being hardened on real networks; ws remains the default and recommended fallback.
By default the SDK carries the conversation over the WebSocket, on TCP. WebRTC (SRTP over UDP, jitter buffering, packet-loss concealment) is a validated alternative for end-user browser apps on weak wifi or cellular. One lost packet no longer stalls every audio frame behind it. The signaling, events and everything else in this guide are unchanged; only the underlying media path differs.
const client = new DialtClient({
url: 'wss://dialt.com/ws',
sessionId: credential.session_id,
apiKey: credential.api_key,
mode: { kind: 'dialt' },
transport: 'webrtc',
});
Safari/WebKit does not yet support the SDK's echo-cancellation path over WebRTC, so the SDK automatically falls back to ws there, no action needed. ws remains the default transport everywhere.
Events
Server JSON events are shared by the browser and Python SDKs. In the browser, read fields from event.detail; in Python, read them from SessionEvent.data. Binary assistant audio and browser transport lifecycle events use the SDK-specific forms below.
| Event | Source | Data | Meaning |
|---|---|---|---|
ready | Server | voice, name, voices, resume_token | The session is accepted and ready. |
silent_mic | Server | reason, duration_ms, peak | Nonfatal capture warning. Check the selected input, hardware mute, and browser and operating-system permissions while keeping the session live. |
turn | Server | welcome? | An assistant reply has started. |
asr | Server | text, turn_id, input_source, message_id? | Final transcript of the caller's spoken or typed turn. |
utterance | Server | text, corrected?, barge_seq? | Assistant text. A corrected event replaces the earlier text for that interruption sequence. |
done | Server | none | The current assistant reply has finished sending. |
session_end_requested | Server | farewell? | The agent called end_call; farewell is the text it just spoke. Stop automatic turn injection; a real user may continue during the close grace period, which cancels the end. |
working | Server | active | Dialt is blocking on a tool result with nothing audible (true), or that wait ended (false). Drives the SDK's thinking sound; useful for a "working…" UI state. |
interrupted | Server | barge_seq, clear? | The caller took the floor and the assistant reply stopped. |
canceled | Server | none | An eager reply was retracted; discard its uncommitted playback. |
tool_call | Server | id, name, args | Run a declared client tool and return its result. |
tool_cancel | Server | id | Cancel the matching tool call if possible. |
tool_deferred_ack | Server | accepted, id, handle?, reason? | Confirms or rejects a deferred acknowledgement. |
tool_deferred_resume | Server | id, handle, name, status_label | Re-associate a deferred host job after reconnect/resume. |
voice | Server | voice, name | A requested voice change was accepted. |
warming_up | Browser SDK only | attempt, device_id | The SDK is opening capture and waiting for its first worklet frame. |
recovering | Browser SDK only | code, device_id? | The SDK is releasing and reacquiring capture after a stall or device change. Do not retry in application code. |
failed | Browser SDK only | code, error | Capture failed terminally. Repeated no-frame startup uses capture_stalled. |
devices_changed | Browser SDK only | devices, device_id, active_device_id | The available audio inputs changed. |
input_device_changed | Browser SDK only | device_id, previous_device_id, reason? | The selected input changed or became unavailable. |
error | Server or SDK | detail or error | The service or SDK rejected or lost the operation. |
audio | SDK from binary frame | Browser: detail.samples and detail.sr. Python: event.audio; rate is OUTPUT_SR. | One Float32 mono assistant-audio frame. |
listening | Browser SDK only | none | The microphone worklet is delivering audio frames; silence is valid. |
reconnecting | Browser SDK only | none | The SDK is recovering an abnormal transport loss. |
reconnected | Browser SDK only | none | The replacement connection resumed the prior conversation and deferred jobs. |
resume_state | Browser SDK only | state | The opaque, persisted continuation state rotated or was cleared. Save non-null state; remove saved state when it is null. |
resume_failed | Browser SDK only | error.code, error.retryable | Terminal resume rejection, including an imported page-reload state. The SDK stops retrying and clears its stale token; the app should end local capture and offer a deliberate fresh start. |
session_end | Browser SDK only | code, reason | A clean server close ended the session. The Python event iterator simply ends. |