Install @dialt/sdk from npm. TypeScript / JavaScript SDK version 0.60.0 adds generated Eleven Realtime Mini and Max target types for hosted voice evaluations. It retains experimental isolated participant audio and transcript provenance for authorized organizations, alongside custom-agent voice evaluations. It versions independently from the Python SDK. To hear Dialt before integrating, open the playground.
The headless @dialt/sdk/node session inherits participants and sendParticipantAudio from the shared client with application-owned isolated audio. Generated transcript, correction and conversation/history types preserve optional participant_id; this is source provenance, never authenticated identity. The existing organization gate and pilot limitations still apply.
Choose an entrypoint
| Import | Use |
|---|---|
@dialt/sdk or @dialt/sdk/browser | Browser realtime with microphone capture, echo cancellation and playback. |
@dialt/sdk/node | Headless Node.js realtime. Your application owns audio capture and playback. |
@dialt/sdk/http | Generated, typed access to the HTTP API from trusted backends. |
@dialt/sdk/evals | Backend workflows for publishing eval cases and planning, starting and waiting for runs. |
@dialt/sdk/tools | JSON-Schema validation and dispatch for application tools. |
The Node.js client uses the same realtime events and methods described below. Import DialtSession, call connect(), send 16 kHz mono audio with streamAudio() or appendAudio(), and consume assistant PCM from audio events. It does not open a microphone, play audio or cancel echo.
import { DialtSession } from '@dialt/sdk/node';
const session = new DialtSession({
apiKey: process.env.DIALT_API_KEY,
outputSampleRate: 24000,
mode: { kind: 'dialt', instructions: 'Help callers with their orders.' },
});
session.addEventListener('audio', ({ detail }) => {
if (detail.samples) play(detail.samples, detail.sr);
});
await session.connect();
await session.streamAudio(inputPcm, { chunkMs: 20 });
Caller input remains mono 16 kHz. outputSampleRate selects 8000, 16000 (the default) or 24000 Hz assistant output and every audio event reports the selected rate in detail.sr. Use 8 kHz before a telephony codec or 24 kHz for wider-band playback. nowMs() returns the session's monotonic clock. For client-owned turn boundaries, set mode.turn_detection to client and call finishInputTurn() after sending a turn's audio. sendClientEvent() sends other documented client_event controls.
Set mode.max_pause_s to configure the detected-silence fallback for server-detected voice turns (default 6, positive finite seconds). Browser and Node clients retain it on reconnect. Earlier endpoint releases remain possible; this is not a minimum thinking pause. Text and client-detected turns reject the override. See maximum detected pause for limits and resume behavior.
The HTTP client is generated from Dialt's OpenAPI contract. Keep its persistent API key on a trusted backend.
import { createClient, listSessions } from '@dialt/sdk/http';
const http = createClient({ auth: process.env.DIALT_API_KEY });
const result = await listSessions({ client: http, query: { limit: 50 } });
Select a stock voice
List the current stock voices on your backend, then pass a returned key in mode.voice or setVoice(). Keep persistent API keys on your backend.
import { createClient, listVoices } from '@dialt/sdk/http';
const http = createClient({ auth: process.env.DIALT_API_KEY });
const { data: roster } = await listVoices({
client: http,
throwOnError: true,
});
// Select a key from roster.voices, for example roster.default.
// Later: mode: { kind: 'dialt', instructions, voice: roster.default }
Migration: Customer voice conditioning and voice creation/deletion are removed. Remove clip-upload and voice-management calls, and replace saved conditioned-voice keys with a current stock key from GET /v1/voices. Stock voice selection remains supported; see the catalogue and migration guide.
The eval workflow wraps generated operations with safe pagination, upsert-by-name, preflight, idempotent run creation and cancellable polling.
import { EvalsClient } from '@dialt/sdk/evals';
const evals = new EvalsClient({ apiKey: process.env.DIALT_API_KEY });
const cases = await evals.listCases();
const plan = await evals.planRun({ case_ids: cases.map(({ id }) => id), targets: ['dialt'] });
const run = await evals.startRun(
{ case_ids: cases.map(({ id }) => id), targets: ['dialt'] },
{ idempotencyKey: crypto.randomUUID(), maxExternalChargeMicros: plan.max_external_charge_micros ?? undefined },
);
await evals.wait(run.id, { signal: AbortSignal.timeout(60 * 60 * 1000) });
Browser quickstart
1. Install the SDK
npm install @dialt/sdk
2. Create a persistent key
Sign in to API & Billing and create a key. New keys begin with dk_; existing ck_ keys remain valid. Keys 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://api.dialt.com/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://api.dialt.com/v1/realtime',
sessionId: credential.session_id,
apiKey: credential.api_key,
mode: {
kind: 'dialt',
instructions: 'You are a helpful voice assistant on a phone call. If a request is missing a key detail, ask one concise clarifying question.',
},
});
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());
mic-worklet.js and track-feeder-worklet.js and resolves them relative to the SDK module. If you serve modules without a bundler, copy the complete installed src/ directory to your own origin; copying only index.js breaks its imports and worklet URLs. Do not depend on a CDN that rewrites the module path without retaining those sibling assets.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.
When rendering transcripts, set dir="auto" and text-align: start on each message's text element so Arabic and other right-to-left languages retain their reading direction and punctuation. Keep speaker labels and controls outside that element. Set text through textContent, isolate names embedded in other text with <bdi>, and keep code and JSON displays left to right.
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 dk_… key | Create scoped browser credentials; connect trusted Python or WebSocket clients. Existing ck_… keys remain valid. | 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 /v1/session-keys
Exchange a persistent key for a browser-safe session credential.
POST https://api.dialt.com/v1/session-keys
Authorization: Bearer dk_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. |
Request a new scoped key with a new session_id for each call. The key expires after 130 minutes and works only with the returned session_id, including for reconnects of that call; 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}.
Session identifiers
| Identifier | Chosen by | What it identifies | Where it appears |
|---|---|---|---|
session_id | You | Your correlation value for one call, such as your own call or ticket ID. Use a new value for each call. Dialt does not enforce uniqueness, so a reused value matches every call that used it and never joins them into one session. | Scoped key exchange, the start frame, Sessions page search |
session_uuid | Dialt | One session, unchanged when the call reconnects, resumes or moves to another server. | ready, client.sessionUuid, the Sessions page, GET /v1/sessions/{uuid} |
Browser SDK reference
DialtClient(options)
| Option | Type / default | Description |
|---|---|---|
url | string, required | Use wss://api.dialt.com/v1/realtime. |
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 | object, required | Conversation configuration with non-empty instructions; 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. At session start, Dialt gives the model the current date, weekday and time in this zone. This is a session-start snapshot. If the zone is missing or invalid, local date and time are unknown. |
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. |
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. |
captureSignalTimeoutMs | number, 5000 | How long a raw (SDK AEC3) capture may deliver only digital silence before listening fires anyway with signal_unverified: true. |
Structured conversation and host control
Pass initialHistory in the constructor to initialize completed user/assistant messages and tool calls/results atomically. This loads history without generating a response or executing historical tools. Developer instructions stay in mode.instructions; Dialt's internal prompt and reasoning are private. client.conversation returns a detached, read-only, revision-aware retained window. Arbitrary history editing is unavailable.
await client.requestReply({ operationId, instructions }) requests one logical response after readiness. Reuse the same operation ID and payload to retrieve the latest queued, started or terminal receipt. Disable the separate automatic greeting for an explicit startup response. client.activeResponseId identifies generation across bridge speech, tool waits and final speech. response_done settles that logical response; done only fences a speech segment. Already received audio can still be playing after either event.
await client.interrupt({ responseId, operationId }) stops exactly that response; targetOperationId instead cancels a queued request. The built-in WebSocket player suppresses and discards only the targeted response, reports discarded playback and preserves newer output. Custom players need response-aware enqueue(samples, sampleRate, responseId), clearResponse(responseId) and pendingMs(responseId) for the same guarantee. Headless integrations own playback and report playback_stopped. These controls require advertised capabilities; targeted interruption is unavailable on WebRTC's independently ordered media transport.
client.activeSpeech and speech_started/speech_stopped represent acoustic activity, separate from final ASR and semantic turn completion. Observation gaps close an episode. They are advisory indicators; use word-level alignment for latency measurements. See the wire contract for limits, revisions and event ordering.
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://api.dialt.com/v1/realtime',
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, including mode.instructions: the SDK requires them on every client, and a failed resume leads to a fresh start that needs them. 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 the server stash survives broker restarts and broker-instance transfers. 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: 'You are the customer support assistant for [company]. Help customers with [company] products and accounts.', // required
greeting: 'Hello!', // string, false, or omit for the default
web_search: false,
end_call: false, // true, or { when: 'the user asks to end the call' }, lets the agent end the session
tools: [],
}
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. See assistant instructions for the mode.instructions contract.
instructions is required and supplies customer-authored developer instructions: the application role, goals, knowledge, policy and task behavior. Dialt's platform prompt combines platform rules, selected voice guidance, and call facts. new DialtClient() throws a TypeError when mode is omitted or its instructions are missing or blank, and the server refuses such a start with missing_instructions. voice selects a synthesis identity with default pronunciation/accent and coupled voice guidance; it does not select the application role. multilingual selects experimental multilingual guidance. These are coupled choices, with no independent style, accent, delivery or customer fine-tuning controls. See the prompt ownership and control limits; intended ownership does not guarantee model obedience.
Use the instruction-authoring recipe to separate shared and active-stage instructions, tool contracts, runtime facts and retrieved knowledge. For calls that switch languages, set mode.multilingual: true on a fresh session; an English instruction alone leaves the selected voice's default-language guidance active. For example:
mode: {
kind: 'dialt',
voice: 'arabic_female',
multilingual: true,
instructions: 'You are the support assistant for [company]. Help with its products. '
+ "Open in Arabic, then follow the caller's language or explicit request.",
}
Remove conflicting rules such as Speak Arabic only. Multilingual is experimental: validate recognition, wording and audio in both languages and across switches on the selected voice. See language switching for configuration, resume and prerecorded-bridge behavior.
The isolated-participant pilot is deployed for the Dialt-organization Owner only. Published SDK support does not broaden access or establish production qualification for other organizations. On a fresh voice session set mode.multispeaker: true, transport: "ws" and top-level participants: [{ id: "alice", name: "Alice" }] (one to four isolated sources). Call session.sendParticipantAudio("alice", frame, { captureMs }) with mono Float32Array, Int16Array or PCM16 Uint8Array at the session rate and uint32 source capture milliseconds. The SDK negotiates participant-pcm16-v1 and independent per-line sequences. Chunk audio to nonempty even PCM payloads of at most 48000 bytes; ordinary microphone/send-audio methods are unavailable for this mode.
The roster and boolean are session-scoped, never saved on a Design, and immutable across resume, including names. Explicit false is preserved on the wire; raw API omission inherits the saved mode and roster. Disconnected audio is dropped. Reset requires a new session. Only WebSocket/server turn detection is supported. Provider endpoints alone are used; single-mic local VAD/silence acceleration and protective sensors are bypassed, and spelling audio is disabled. participant_id survives transcript/correction and native user-message events, including imported history; native assistant role remains authoritative. See the linked protocol for classification semantics, framing and known release gaps. Hosted evals and production qualification remain unsupported.
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 with Dialt's default ending condition (the caller asks you to end the call); { when: '<condition>' } states your own, declared to the agent as part of the tool. On the turn that calls it, Dialt sends one goodbye. Reply text in that turn that ends a sentence is the goodbye, and the farewell is not sent after it. Reply text that stops mid-sentence is followed by the farewell, which completes it. With no reply text, the farewell is the goodbye. Text the agent streamed before calling end_call follows the same rule, and the farewell is sent if none of that text was heard. Dialt then sends advisory session_end_requested with the farewell argument. Neither done nor that advisory confirms that the peer heard or acted on the farewell. Stop automatic new-turn injection, keep any queued output, and let the client's normal-close path drain local playback if needed. The server's short grace is cancellable by a real user turn, then it closes if the request remains active. 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.
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: 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. |
continueWithAgent({operationId, instructions, tools, voice?, context?, toolChoice?, toolChoiceOneShot?}) | Promise<ack> | Atomically initializes the next agent. It resolves when the broker applies or rejects the continuation, within 10 seconds; it does not start speech. handoffAgent() remains a compatibility alias. |
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 | Gates current and future SDK-owned microphone tracks. The setting persists across capture restart, device fallback and transport reconnection until changed explicitly. |
reset() | Promise | Ends the old conversation and opens a fresh session with the current configuration. Keeps microphone capture running where possible. The new ready event has a new session UUID. Optional { sessionId, apiKey } replaces a session-scoped credential pair. |
setInstructions(text, {newSpeaker?}) | boolean | Replaces the session instructions beginning with the next reply. newSpeaker: true remains available for an independent speaker change. Refused with instructions_busy while a reply is in flight; send it after done. |
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. |
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. Send outcome and verified whenever your application can establish them; report the outcome explains how each value shapes the reply. |
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 caller turn, and oneShot reverts after the next caller or explicitly host-requested reply consumes it. Autonomous jobs do not consume it. Also available at session start as mode.tool_choice. |
setTools(tools) | boolean | Replaces the client tool manifest mid-session (same shape as mode.tools) for an independent capability change; managed tools ride along and tool_choice resets to "auto". |
resolveToolPermission(id, decision) | boolean | Submits 'approve' or 'decline' for an external permission request. Returns whether the transport accepted the frame. Check permission_resolution.accepted for the broker decision. |
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.
To continue at an idle boundary, await continueWithAgent() until status: 'applied', then separately call injectContext(..., { role: 'context', reply: true }) when the initialized agent should speak without a caller turn. When a tool result is the boundary, prefer sendToolResult(..., { continueWith: {...} }); it records the result, initializes the next agent and requests its reply atomically. Include toolChoice and toolChoiceOneShot: true when the incoming reply must call a grounding tool first; Dialt applies it before starting that reply and restores auto afterward. One continuation may be pending. The caller wins a race with a separate reply request. Continuations are not retried or restored after reconnect; use a new operation ID each time.
Send outcome and verified whenever your application can establish them. { outcome: 'succeeded', verified: true } lets the assistant confirm the action ("You're booked for Tuesday"); send it when you hold proof, such as a confirmation ID from your booking API. { outcome: 'unknown' }, the default, makes the assistant say it cannot confirm the action yet, which suits queued or fire-and-forget work. { outcome: 'failed' } makes it say the action did not go through. verified: true is valid only with outcome: 'succeeded'. Invalid combinations throw a TypeError before any result is sent, so handle execution errors and always send a terminal result for the call. See report the outcome.
client.addEventListener('tool_call', async ({ detail }) => {
let content;
try {
content = await executeTool(detail.name, detail.args);
} catch (error) {
// Record diagnostic details in your application; return only safe error data.
client.sendToolResult(detail.id, { error: 'tool_execution_failed' },
{ outcome: 'failed', verified: false });
return;
}
if (content.confirmationId) {
// Proof of completion: the assistant can confirm the action.
client.sendToolResult(detail.id, content, { outcome: 'succeeded', verified: true });
} else {
// Queued or fire-and-forget: the assistant says it cannot confirm yet.
client.sendToolResult(detail.id, content, { outcome: 'unknown' });
}
});
Keep results compact. Above the default 16 KiB JSON limit, the model receives a bounded result containing tool_result_truncated: true, original_bytes and preview. Return a summary and relevant structured facts when the full result is larger.
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.
Application tool approval
Declare a tool with requires_permission: true and permission_source: 'application' to require your application's approval. Omitting the source retains caller approval verified by AI. The assistant does not ask the caller to authorize an application request. Your application authenticates the approver and submits the decision on the session's authenticated connection. Only give this connection to a client trusted to approve its tools; for a separate staff approver, keep the approving connection in your backend.
// approvalApp is your application's authenticated review integration.
client.addEventListener('permission_pending', ({ detail: request }) => {
if (request.source === 'application') {
approvalApp.showRequest(request); // display exact tool, arguments and expiry
}
});
approvalApp.onDecision(({ id, decision }) => {
client.resolveToolPermission(id, decision); // 'approve' or 'decline'
});
client.addEventListener('permission_resolution', ({ detail: ack }) => {
approvalApp.showResolution(ack.id, ack.accepted, ack.reason);
});
Sending a decision does not mean the action completed. An accepted approval releases the stored action; handle its tool_call and send its result normally. Requests expire and are single-use. An ID identifies the exact stored arguments; changing those arguments requires a new request.
Properties: client.mode is the active mode configuration; client.responding is true while an assistant reply is active; client.sessionUuid is null until ready, then the server-minted session identifier. It is the ID the Sessions page searches and shows, and the one GET /v1/sessions/{uuid} takes. A terminal resume_failed clears it along with the resume state, so store it when ready arrives if you need to look the call up later.
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, 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 and provides echo cancellation through native browser AEC where supported and SDK AEC3 on WebKit. Noise suppression and automatic gain control remain disabled. It resolves only after the worklet supplies an actual frame. 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'.
A resolved startMic() does not yet mean the user can speak. listening fires when capture is proven live: any frame on native AEC, and on WebKit's raw capture (SDK AEC3) a frame that is not digital silence, because WebKit can deliver silent frames while the device is still starting. During a call, recovering with code: 'capture_stall' means no frames arrived for 300 ms, and code: 'capture_silent' means WebKit's raw capture delivered 300 ms of digital silence; listening fires again once audio returns. Frames are always uplinked, whatever the state.
Use warming_up, recovering, listening, and failed to render status, and invite speech only on listening. 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.
const inputs = await client.getInputDevices();
renderInputPicker(inputs);
inputPicker.addEventListener('change', async () => {
await client.setInputDevice(inputPicker.value || null);
});
client.addEventListener('devices_changed', ({ detail }) => {
renderInputPicker(detail.devices, detail.device_id);
});
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 compatibility
The table lists Browser SDK compatibility targets. It does not certify production use on each device; 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. |
Embedded WKWebView apps require separate validation from Safari. Validate microphone permissions, asset loading and acoustic behavior in the host app before committing to that integration. On WebKit, the SDK normally uses its own AEC3 echo canceller; native platform cancellation is a fallback if AEC3 fails to initialize.
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://api.dialt.com/v1/realtime',
sessionId: credential.session_id,
apiKey: credential.api_key,
mode, // the same mode as the WebSocket example, instructions included
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, session_uuid? | The session is accepted and ready. The SDK sets client.sessionUuid from session_uuid, the server-minted session identifier that stays the same across resumes and broker transfers. |
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. It is not a playback acknowledgement. |
session_end_requested | Server | farewell? | Advisory that the agent called end_call; farewell is its farewell argument, which is not spoken when the reply text of that turn ended a sentence. Stop automatic turn injection but keep queued output. This does not confirm that the peer heard or acted on the farewell; a real user turn during the server grace 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? | Capture is not live: the SDK is reacquiring it after a startup stall (capture_stalled) or device change, or the device stopped delivering audio mid-call (capture_stall, capture_silent). Do not invite speech or 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 and event.audio_sample_rate. | One Float32 mono assistant-audio frame at the session's selected output rate. |
listening | Browser SDK only | device_id, recovered, signal_unverified? | Capture is live and speech can be invited. On WebKit's raw capture this needs a frame that is not digital silence; signal_unverified marks a device accepted after captureSignalTimeoutMs of silence. |
capture_stall | Browser SDK only | stall_ms | Diagnostic: the audio thread lost this much capture time, and the SDK advanced its capture timestamps to page time so recordings show a gap instead of shifting later audio earlier. |
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. |