Dialt
Get API key

Developer guide

Browser SDK

The recommended path for web applications. The SDK owns microphone capture, browser echo cancellation, playback, reconnects and structured events.

← Choose another integration

Install @dialt/sdk from npm. To hear Dialt before integrating, open the playground.

Browser quickstart

1. Install the SDK

npm install @dialt/sdk
Migrating to Dialt: replace @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

CredentialUseWhere it belongs
Persistent ck_… keyCreate scoped browser credentials; connect trusted Python or WebSocket clients.Backend or trusted service only.
Scoped session keyAuthenticate 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
}
StatusMeaning
201Credential created.
400Invalid or missing session_id.
401Persistent key missing, unknown or revoked.
415Request 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)

OptionType / defaultDescription
urlstring, requiredUse wss://dialt.com/ws.
sessionIdstring, generatedMust match a scoped key's bound session ID.
apiKeystringScoped browser key or, in trusted environments only, a persistent key.
mode{kind:'dialt'}Conversation configuration; see Mode options below.
userstringOptional stable identifier for your user. It is metadata, not authentication.
timezonestringOptional IANA timezone such as Europe/Dublin.
playerStreamingPlayerOptional caller-supplied playback implementation.
resumeStateobject | null, nullOpaque state previously returned by exportResumeState(). Imports it before the first connection.
autoReconnectboolean, trueReconnect after abnormal transport loss and resume the prior conversation while the server token remains valid.
reconnectBaseMsnumber, 500Initial reconnect delay.
reconnectMaxMsnumber, 5000Maximum reconnect delay.
maxReconnectAttemptsnumber, 12Attempts before a terminal error.
rawAssistboolean, falseSupport diagnostic for comparing processed and unprocessed microphone audio. Leave disabled unless Dialt support asks you to enable it.
inputDeviceIdstring | null, nullPreferred microphone device ID; null follows the system default.
captureStartupTimeoutMsnumber, 2000Bound 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

MethodReturnsDescription
unlockAudio()PromiseUnlocks browser playback. Call inside the Start button's user gesture.
connect(options?)Promise<DialtClient>Connects and resolves after ready. Options: temperature, noGreeting.
exportResumeState()object | nullReturns 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)booleanCommits one typed user turn in modality: 'text'. The ordinary asr, turn, text_delta, utterance and done events follow, without audio.
importResumeState(state)voidImports 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()PromiseReleases 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)voidTemporarily gates SDK-owned microphone tracks.
reset()PromiseClears playback and starts a fresh conversation on the same connection.
setVoice(key)voidChanges the voice beginning with the next assistant reply.
close()voidStops capture and playback and closes the socket.
closeAndWait(timeoutMs?)PromiseCloses and waits for the WebSocket close handshake.
pushMicFrame(frame)voidAdvanced: sends a caller-owned Float32 mono 16 kHz frame on an already-live session.
appendAudio(frame)PromiseAdvanced: connects if needed, then sends one caller-owned Float32 frame.
sendRawFrame(frame)voidSupport diagnostic: sends a synchronized unprocessed frame when rawAssist is enabled.
sendAmbienceState(active)voidRecords whether your client-side ambience layer is active; it does not alter server audio.
requestWrapUp(reason)voidAsks 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 })voidResolves a tool_call; only outcome: 'succeeded' with verified: true authorizes success narration.
sendToolDeferred(id, { handle, statusLabel? })voidReleases an eligible long-running call from the voice turn while preserving its managed job identity.
sendToolProgress(id, note)voidReports human-readable progress on an in-flight call; never resolves it.
sendToolPartialResult(id, content, {reply, interaction})voidDelivers 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?})booleanRestricts 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)voidCancels 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.

Python and raw WebSocket integrations do not include a device media stack. For speakerphone use, add platform or telephony echo cancellation. Otherwise use headphones or disable the microphone during playback; disabling it prevents the caller from interrupting the assistant.

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.

PlatformAPI compatibility targetsPlayback / capture path
macOSChrome, Brave, Firefox, SafariNative browser AEC except Safari, which uses the SDK's WebKit path.
WindowsChrome, Brave, Edge, FirefoxNative browser AEC.
AndroidChrome, Brave, FirefoxNative browser AEC; device audio policy remains browser/OS-controlled.
iPhone / iPadSafari and current Chrome/Brave buildsSDK 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.

EventSourceDataMeaning
readyServervoice, name, voices, resume_tokenThe session is accepted and ready.
silent_micServerreason, duration_ms, peakNonfatal capture warning. Check the selected input, hardware mute, and browser and operating-system permissions while keeping the session live.
turnServerwelcome?An assistant reply has started.
asrServertext, turn_id, input_source, message_id?Final transcript of the caller's spoken or typed turn.
utteranceServertext, corrected?, barge_seq?Assistant text. A corrected event replaces the earlier text for that interruption sequence.
doneServernoneThe current assistant reply has finished sending.
session_end_requestedServerfarewell?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.
workingServeractiveDialt 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.
interruptedServerbarge_seq, clear?The caller took the floor and the assistant reply stopped.
canceledServernoneAn eager reply was retracted; discard its uncommitted playback.
tool_callServerid, name, argsRun a declared client tool and return its result.
tool_cancelServeridCancel the matching tool call if possible.
tool_deferred_ackServeraccepted, id, handle?, reason?Confirms or rejects a deferred acknowledgement.
tool_deferred_resumeServerid, handle, name, status_labelRe-associate a deferred host job after reconnect/resume.
voiceServervoice, nameA requested voice change was accepted.
warming_upBrowser SDK onlyattempt, device_idThe SDK is opening capture and waiting for its first worklet frame.
recoveringBrowser SDK onlycode, device_id?The SDK is releasing and reacquiring capture after a stall or device change. Do not retry in application code.
failedBrowser SDK onlycode, errorCapture failed terminally. Repeated no-frame startup uses capture_stalled.
devices_changedBrowser SDK onlydevices, device_id, active_device_idThe available audio inputs changed.
input_device_changedBrowser SDK onlydevice_id, previous_device_id, reason?The selected input changed or became unavailable.
errorServer or SDKdetail or errorThe service or SDK rejected or lost the operation.
audioSDK from binary frameBrowser: detail.samples and detail.sr. Python: event.audio; rate is OUTPUT_SR.One Float32 mono assistant-audio frame.
listeningBrowser SDK onlynoneThe microphone worklet is delivering audio frames; silence is valid.
reconnectingBrowser SDK onlynoneThe SDK is recovering an abnormal transport loss.
reconnectedBrowser SDK onlynoneThe replacement connection resumed the prior conversation and deferred jobs.
resume_stateBrowser SDK onlystateThe opaque, persisted continuation state rotated or was cleared. Save non-null state; remove saved state when it is null.
resume_failedBrowser SDK onlyerror.code, error.retryableTerminal 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_endBrowser SDK onlycode, reasonA clean server close ended the session. The Python event iterator simply ends.