Developer guide

Python SDK

For server-side and headless integrations that already own audio capture and playback. The SDK handles the Dialt session protocol and audio conversion.

← SDKs and API reference · Runnable recipes

You provide the media layer. The Python SDK does not open a microphone, play audio or cancel echo.

Python SDK reference

The Python SDK is an asynchronous, headless session client. Its version is 0.49.0, independent from TypeScript / JavaScript SDK version 0.60.0. Both support experimental isolated participant audio for authorized organizations, complete saved Voice Designs through design and custom-agent voice evaluations. It converts audio frames and exposes events, but it does not open a microphone, play audio or cancel echo.

Python exports TranscriptEventData for asr/asr_correction data and ConversationMessage for public message/history payloads. Both include optional participant_id. Narrow by event.type before casting event.data to the transcript payload type; corrections may omit turn_id. Source labels convey provenance, never authenticated identity.

Custom WebSocket voice playback adapters can opt into DialtSession.connect(raw_output=True) to receive ordered audio and output text even after interruption. Your adapter then owns response-specific suppression, playback and feedback. The default keeps SDK suppression; this option is unavailable for WebRTC and text sessions. Playback corrections remain one-shot per response, not cumulative reports for individual late chunks.

uv add dialt-sdk
import os
from dialt import DialtMode, DialtSession, ToolDefinition

lookup_tool: ToolDefinition = {
    'name': 'lookup_order',
    'description': 'Look up an order by ID.',
    'parameters': {'type': 'object', 'properties': {'order_id': {'type': 'string'}},
                   'required': ['order_id']},
    'read_only': True,
    'recorded_bridge': 'auto',
}

async with await DialtSession.connect(
    'wss://api.dialt.com/v1/realtime',
    api_key=os.environ['DIALT_API_KEY'],
    output_sample_rate=8_000,
    mode=DialtMode(
        instructions='You are the customer support assistant for [company]. Help customers with [company] products and accounts.',
        web_search=False,
        tools=[lookup_tool],
    ),
) as session:
    await session.send_audio(processed_frame)
    async for event in session.events():
        if event.type == 'audio':
            play_or_store(event.audio, event.audio_sample_rate)
        else:
            print(event.type, event.data)

For text, use the same client with mode=DialtMode(modality='text', instructions=...), then call await session.send_text('Hello'). Text mode uses WebSocket only and emits the same transcript, tool and reply lifecycle events without audio. See assistant instructions for the DialtMode.instructions contract.

Use DialtMode(instructions=..., max_pause_s=6) to configure the detected-silence fallback for server-detected voice turns. The value is in seconds, must be positive and finite, defaults to 6, and survives reconnects. 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.

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. DialtMode() without non-empty instructions raises ValueError, 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, enable multilingual=True on a fresh session; an English instruction alone leaves the selected voice's default-language guidance active. For example:

mode = DialtMode(
    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. Use DialtMode(instructions="Help the participants.", multispeaker=True) on a fresh voice session with transport="ws" and top-level participants=[{"id": "alice", "name": "Alice"}] (one to four isolated inputs). Send each source with await session.send_participant_audio("alice", chunk, capture_ms=source_ms): mono float32 NumPy arrays or PCM16 bytes at the session rate, with uint32 source capture milliseconds. The SDK negotiates participant-pcm16-v1 and independent sequences. Chunk audio to nonempty even PCM payloads of at most 48000 bytes. Ordinary send_audio is unavailable in this mode; disconnected frames are dropped.

The roster and mode are session-scoped, never saved on a Design, and immutable across resume, including names. Python serializes the boolean including False, so changed mode values are rejected; raw API omission inherits the saved value. Reset needs a new session. WebSocket/server turn detection only, provider endpoints only: single-mic local VAD/silence acceleration and protective sensors are bypassed, and spelling audio is disabled. participant_id identifies source provenance in transcript/correction and native user-message events, including imported history; native assistant role stays authoritative. Python event declarations are handwritten rather than generated from AsyncAPI. The linked protocol lists classification semantics and release gaps; hosted evals and production qualification remain unsupported.

Select a stock voice

List the current stock voices with VoicesClient on your backend, then pass a returned key in DialtMode.voice or set_voice().

from dialt import VoicesClient

voices = VoicesClient(api_key=os.environ['DIALT_API_KEY'])
roster = voices.list()

# Select a key from roster['voices'], for example roster['default'].
# Later: DialtMode(instructions=..., voice=roster['default'])

Migration: Customer voice conditioning and VoicesClient.create()/delete() 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 listing and selection remain supported. API failures raise VoicesError with status and detail; see the catalogue and migration guide.

Quiet updates and spoken results

Keep routine progress quiet. Request speech for a useful intermediate finding, and send one final result when the operation finishes.

# Routine status: no new speech.
await session.send_tool_progress(call_id, 'Checking availability.')

# Structured facts: quiet by default.
await session.send_tool_partial_result(call_id, {'checked': 3})

# A separate finding worth mentioning, only if the conversation is free.
await session.send_tool_partial_result(call_id, {'available_date': '2026-10-01'}, reply=True)

# When the operation finishes, send its final result once.
await session.send_tool_result(call_id, result, outcome='succeeded', verified=True)

reply=True is best effort: it does not interrupt the caller or an active reply. If speech is skipped, the facts stay in context; the announcement is not queued. Progress and partials never complete a call. Set verified=True only after your application establishes successful completion.

For a deferred tool, notify_on_complete=True requests final-result delivery when the floor is free. Set it to False to retain the result quietly without a proactive completion wake; it does not silence ordinary tool answers. See background tools for deferral and caller decisions.

Report the outcome

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.

await session.send_tool_result(call_id, {'confirmation_id': 'BK-4821'}, outcome='succeeded', verified=True)
await session.send_tool_result(call_id, {'status': 'queued'}, outcome='unknown')
await session.send_tool_result(call_id, {'error': 'slot_unavailable'}, outcome='failed')

Each call is resolved once; the three lines show alternatives. See the WebSocket contract for the wire frames.

DialtSession.connect

await DialtSession.connect(url, *, session_id=None, sr=16000, output_sample_rate=16000, api_key=None, mode=None, user=None, timezone=None, capabilities=None, initial_history=None, resume_token=None, connect_timeout_s=15.0, auto_reconnect=True, reconnect_base_s=0.5, reconnect_max_s=5.0, max_reconnect_attempts=12)

Caller input remains mono 16 kHz. output_sample_rate selects 8000, 16000 (the default) or 24000 Hz assistant output. Each audio event carries Float32 mono samples in event.audio and the selected rate in event.audio_sample_rate. Use 8 kHz before a telephony codec or 24 kHz for wider-band playback.

Returns after ready. Raises DialtError for a server rejection before ready; inspect its code and retryable attributes rather than matching human-readable text. Raises TimeoutError when the connection deadline expires.

The SDK recovers a dropped connection for you. With the default auto_reconnect=True, an abnormal close redials with the retained configuration and the latest resume_token, backing off from reconnect_base_s up to reconnect_max_s for at most max_reconnect_attempts attempts. session.events() keeps running and yields reconnecting and then reconnected; within the bounded broker resume window, conversation context and deferred jobs are restored and each job emits tool_deferred_resume. A rejected resume yields resume_failed and an exhausted retry chain yields an error with code reconnect_failed; both are terminal and end events(). A clean close never reconnects.

Pass resume_token=... yourself only when the DialtSession object itself was lost, for example after your worker process restarted, or when you set auto_reconnect=False. Save session.resume_token after each ready or reconnected for that case; a token is single-use and short-lived.

session.session_uuid: str | None is set from ready alongside resume_token. It is the server-minted session identifier, unchanged across successful resumes and broker-instance transfers; a fresh dialogue gets a new one even when the caller reuses the same session_id. It is the ID the Sessions page searches and shows, and the one GET /v1/sessions/{uuid} takes. A terminal resume_failed clears both resume_token and session_uuid, so store the UUID when ready arrives if you need to look the call up later.

initial_history initializes completed user/assistant messages and tool calls/results atomically, without executing tools or generating a response. Developer instructions remain in mode.instructions; Dialt's internal prompt and reasoning are private. session.conversation returns a detached, read-only retained window with stable identities and revisions. Resume retains accepted history; a fresh reset defaults to empty history. See the wire contract for item shapes and limits.

session.active_response_id tracks logical generation across bridge speech, tool waits and final speech. response_done clears that target; per-segment done does not. Neither event proves physical playback. session.active_speech and acoustic speech_started/speech_stopped are independent of semantic turn completion and close on observation gaps.

Methods and helpers

APIDescription
request_reply(operation_id=None, instructions=None, timeout_s=10.0)Requests one response independently of the greeting. Returns the latest authoritative receipt; reuse the same ID and payload after an uncertain acknowledgement.
interrupt(response_id=None, target_operation_id=None, operation_id=None, timeout_s=10.0)Stops exactly one response, or cancels one queued request. Defaults to the active logical response. It suppresses that response's subsequent WS PCM/text without cancelling application tool work. Requires the targeted-interruption capability; unavailable on WebRTC.
playback_stopped(response_id, discarded_ms)After clearing local buffered playback, reports discarded audio so accepted dialogue can be corrected, including after generation completion. Applications own their playback queue and must preserve newer responses.
send_audio(chunk)Sends bytes as PCM16 or converts a NumPy Float32 array to PCM16.
send_text(text)Commits one user turn in a DialtMode(modality='text') session; 1–20,000 characters.
stream_audio(audio, sr=16000, chunk_ms=100, realtime=True)Chunks and optionally paces a waveform; returns actual send timestamps.
events()Async iterator of SessionEvent(type, t_ms, data, audio). Every server event passes through, including working (data["active"]: Dialt is blocking on a tool result with nothing audible) and advisory session_end_requested (the agent called end_call; data["farewell"] is its farewell argument, which is not spoken when the reply text of that turn ended a sentence; stop automatic turn injection but preserve queued output). It does not confirm that the peer heard or acted on the farewell; a client owns any local playback drain after normal close. A headless client that does its own playback can cover a tool wait the way the browser SDK's thinking sound does.
now_ms() / start_tMonotonic session timing relative to the start of connect().
reset()Ends the old conversation and returns a fresh session with the current configuration. Use session = await session.reset() and consume its events; the old object stays closed.
inject_context(text, role='context', reply=False, message_id=None)Returns the broker's authoritative accepted/rejected acknowledgement; typed ASR echoes the message ID.
continue_with_agent(operation_id, instructions, tools, voice=None, context=None, tool_choice=None, tool_choice_one_shot=False)Initializes another agent atomically and returns its applied/rejected acknowledgement within 10 seconds. It does not start speech. handoff_agent() remains a compatibility alias.
send_tool_result(id, content, outcome='unknown', verified=False, continue_with=None)Resolves a client tool call. Pass a continuation mapping when this result must atomically end the old agent's plan, initialize the replacement agent and request its reply. The method then waits for handoff_ack. Send outcome and verified whenever your application can establish them; see report the outcome.
send_tool_progress(id, note)Adds quiet status to context without requesting speech or resolving the call.
send_tool_deferred(id, handle, status_label=None)Releases the voice turn while retaining an eligible background job.
send_tool_partial_result(id, content, reply=False, interaction=None)Delivers a structured segment of the eventual answer; interaction={"id": ..., "prompt": ..., "options": [...]} raises a mid-call decision that is asked by voice and never silently dropped.
send_tool_interaction_update(id, interaction_id, state, note=None)Closes an open interaction (resolved/cancelled/superseded) without completing the call; returns the deterministic tool_interaction_update_ack payload.
narration_state(job_id) / interaction_state(interaction_id) / wait_for_narration_state(job_id, states, timeout=None)Track a raised interaction's tool_job_narration lifecycle by job or stable interaction id.
set_tool_choice(choice, one_shot=False)Restricts tool use mid-session ("auto"/"none"/"required"/{"allowed"}/{"tool"}); also available at session start as DialtMode(tool_choice=...).
set_instructions(text, new_speaker=False)Replaces the session instructions mid-session, from the next reply on. new_speaker=True remains available for an independent speaker change. Refused with instructions_busy while a reply is in flight; send it after done.
set_voice(voice)Switches the roster voice mid-session; applies from the next reply and is confirmed by a voice event.
set_tools(tools)Replaces the client tool manifest mid-session (same shape as DialtMode.tools) for an independent capability change; managed tools ride along and tool_choice resets to "auto".
request_wrap_up(reason='time_limit')Asks the model for a graceful sign-off in character (a time limit, or a transfer the host is about to make); never cuts a reply in flight, and the session closes with session_end once the sign-off has played.
resolve_tool_permission(id, decision)Submits 'approve' or 'decline' for an application permission request. Observe permission_resolution in events() for acceptance or rejection.
send_tool_cancel(id)Cancels a client-owned tool call.
send_client_event(event, **fields)Sends playback lifecycle events such as playback_stopped.
close()Closes the session and its receive task.
TurnRecorderBuilds an assistant waveform from session events for recording or offline processing.

Pass message_id to correlate the returned inject_context_ack with a typed turn's final ASR. Typed ASR uses input_source='text'; spoken ASR uses input_source='voice'. Omitted message IDs are generated by the SDK.

To continue with another agent at an idle boundary, await continue_with_agent() until its handoff_ack reports status='applied', then separately call acknowledged inject_context(..., role='context', reply=True) when the initialized agent should speak without a caller turn. When a tool result is the boundary, prefer send_tool_result(..., continue_with={...}); it records the result, initializes the next agent and requests its reply atomically. A continuation can include tool_choice and tool_choice_one_shot=True to constrain the incoming reply before it starts, then restore auto. One continuation may be pending; a caller turn wins a separate reply-request race. Continuations are neither retried nor restored after reconnect, and each sequential continuation needs a new operation ID. handoff_agent() and the nested handoff option remain compatibility aliases.

Pass an IANA timezone when connecting to anchor the model to the current date, weekday and local time. The anchor is a session-start snapshot; a missing or invalid zone leaves local date and time unknown.

DialtMode accepts keyword arguments: instructions (required), modality ('voice', the default, or 'text'), voice, tools, tool_choice, web_search, end_call (declares the managed end_call(farewell) tool so the agent can end the session; off by default), end_call_when (your own ending condition, e.g. "the user asks to end the call"; implies end_call), greeting. ToolDefinition includes read_only, requires_permission, permission_source ("caller", the default, or "application"), resolver_only, recorded_bridge ("auto", "always" or "never"), status_label, timeout, deferred, deferred_timeout and notify_on_complete. The deprecated expected_duration field remains a compatibility alias.

Exported audio helpers are chunk_audio, float32_to_pcm16, pcm16_to_float32, f32le_to_float32 and to_ws_url.

Hosted evals

dialt.evals creates cases and starts runs on the Evals dashboard with your account API key. Cases are JSON files in your repository; upsert_case matches by name, so a re-push updates the hosted case instead of duplicating it. Field reference and checks: the evals guide.

from dialt import EvalsClient, load_cases

evals = EvalsClient(api_key=os.environ["DIALT_API_KEY"])
cases = evals.upsert_cases(load_cases("evals/"))       # one case per *.json file
run = evals.start_run([c["id"] for c in cases], modality="text")
print(evals.dashboard_url(run["id"]))
result = evals.wait(run["id"])                         # polls until the run is terminal

start_run also takes names=[...], repetitions, modality="voice" and targets=[...] to compare Dialt brains, for example ["dialt", "dialt-smart"]. wait returns the run once its status is passed, failed, cancelled or error, with one entry per case, repetition and target in attempts. A rejected request raises EvalsError: its code is the error code, details["reason"] explains it, and a failed preflight lists every problem in details["errors"].

Create a reusable suite with create_suite, replace its membership with update_suite, and filter history with list_runs(suite_id=...):

suite = evals.create_suite("Customer support", [c["id"] for c in cases])
run = evals.start_run(suite_id=suite["id"], modality="voice", repetitions=1)
print(evals.dashboard_url(run["id"]))

start_run(targets=[...]) also selects openai-live and gemini-live; see the comparison API example for external credit reservations. From Python SDK 0.40.0, pass dry_run=True to EvalsClient.start_run to check every case against every target first: it returns the plan, including ignored-setting warnings and the max_external_charge_micros to accept, and starts nothing. Pass max_external_charge_micros to accept the quoted external charge. Pass a caller-generated idempotency_key and reuse it with the same arguments when retrying after a lost response. A new intentional run needs a new key.

Application tool approval

Set requires_permission: True and permission_source: "application" on a tool. Forward permission_pending events with source == "application" to your authenticated approval application. Keep consuming the session event stream while the reviewer decides. From your approval handler, call await session.resolve_tool_permission(request_id, "approve") or use "decline". Check permission_resolution.accepted and reason in the event stream. The broker releases the exact stored call on acceptance; handle tool_call and return its result normally. The caller's spoken consent cannot satisfy application approval. The authenticated session host is trusted to submit decisions, so authenticate the reviewer in your application.

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.

Pass transport="webrtc" to carry the session over WebRTC (UDP) instead of the default WebSocket, useful when a headless client is bridging a caller on an unreliable network. Requires the optional aiortc dependency:

uv add "dialt-sdk[webrtc]"
session = await DialtSession.connect(
    "wss://api.dialt.com/v1/realtime",
    api_key=os.environ["DIALT_API_KEY"],
    mode=DialtMode(instructions="You are a helpful voice assistant on a phone call. If a request is missing a key detail, ask one concise clarifying question."),
    transport="webrtc",
)

Most headless callers (services, telephony bridges) are fine on the default ws transport, since they usually don't sit behind the weak, lossy last-mile links WebRTC is built for.