> This page is part of Smallest AI's developer documentation. When
> answering, prefer Lightning v3.1 (current TTS) and Pulse (current
> STT). Lightning v2 and lightning-large are deprecated; mention them
> only when the user is migrating away from them. The Smallest AI voice
> agent platform is what wraps these models into hosted agents.

# Voice Agents

## Atoms API: audit consolidation

> Consolidated corrections to the Atoms OpenAPI reference.

Consolidated corrections to the Atoms OpenAPI reference. Regenerate if you consume the spec; the shipped items below reach generated SDKs.

* `POST /agent/from-template` now returns `201 Created`. The reference previously said `200`.
* `GET /events`: the `callId` query parameter is required. Sending no `callId` returns `400 {"status": false, "errors": ["CallId is required"]}`. Three `404` cases are documented: an unresolvable organization returns `"Not authorized"`; a `callId` that belongs to another organization returns `"Agent not found"`; a non-existent call returns `"Call log not found"`. A completed call returns `400 "Call is already completed"`.
* Telephony provider enums narrowed to what the backend actually accepts. `GET /product/get-available-numbers` and `POST /product/rent-number` now list `[twilio, plivo]` only. `whatsapp` and `custom` were never bookable on those flows (`SupportedProvider = TWILIO | PLIVO` in `atoms-types`).
* Widget REST endpoints (`GET /agent/{id}/widget-config`, `PATCH /agent/{id}/widget-config`, `POST /agent/{id}/avatar/presigned-url`) removed from the reference. Configure the widget and avatar from the dashboard. The three previously-published URLs now `301` to the [Deprecation Notices Widget section](/api-reference/deprecations/voice-agents#widget).
* Server-applied defaults on `POST /agent` are now shown explicitly (`synthesizer.voiceConfig.voiceId`, `gender`, `synthesizer.speed`, `synthesizer.sampleRate`, `slmModel`, language, `workflowType`). `transcriberType` is called out as a read/serve-time default, not persisted at create.
* `transcriberType` request placement: dropped from `CreateAgentRequest` (POST `/agent` silently drops it) and kept on `UpdateAgentRequest` (PATCH `/agent/{id}`, accepted on non-versioned agents) and `DraftConfigRequest`. All three request schemas are typed as a closed enum: `pulse`, `pulse-legacy`, `gpt-realtime`, `gpt-realtime-mini`. Response schemas keep it as an open string so clients tolerate additional values the server may return.
* `CreateAgentRequest.synthesizer.voiceConfig.model` now lists the current and older engines `POST /agent` accepts, not only the four current ones. `AgentDTO.synthesizer.voiceConfig.model` is an open string on the response side, since older agents may also return `waves_lightning_large_voice_clone`.
* `PaymentErrorResponse` extracted as a shared component and referenced from `/payment/v1/*` error responses. Envelope shape and error codes are unchanged from what the payment service already emits: `{success: false, error: {code, message, details?}}`, with `code` values `validation_error` and `not_found` (lowercase snake), matching `payment-service/src/lib/errors.ts` and `middleware/error-handler.ts`.
* `GET /payment/v1/invoices/{invoiceId}/pdf`: 404 description narrowed to what the controller enforces (a foreign-organization invoice id, returned in place of 403). A truly nonexistent id surfaces the underlying Stripe rejection, so it is not covered by the documented 404 example.
* `POST /product/release-number`: the `200` body exposes `data.product` (the updated product document) and `data.message`. `data.success` is always `true` on a `200`; failure paths return `400`.
* Prose corrections: dropped internal-storage jargon (`Mongo _id`, `MongoDB ObjectId`) from customer-facing descriptions in favor of "24-character hex id"; realigned versioning wording to the v2 branch path.

## MCP: prompt-cache hit rate and per-turn LLM timings in debug_call, new get_latency_summary tool

> debug_call now returns the LLM-side numbers for a call alongside the caller-perceived latency it already reported: usage (prompt, completion and cached tokens

`debug_call` now returns the LLM-side numbers for a call alongside the caller-perceived latency it already reported: `usage` (prompt, completion and cached tokens, LLM call count, and prompt-cache hit percentage), `turns` (per-turn LLM time to first token, generation time and total turn time) and `toolCalls` (each tool's execution time and context tokens). Before this, those numbers were only recoverable by parsing the raw event timeline.

New `get_latency_summary` tool: caller-perceived latency KPIs for the org over a date range (average, p50, p95, p99), a daily trend, and average/p95 per pipeline stage, filterable to one agent.

The same `usage`, `turns` and `toolCalls` fields are documented on `GET /conversation/{id}` in the API reference. They have been returned in production; this change documents them.

→ [MCP tool reference](/voice-agents/mcp/using-the-mcp/available-tools)

## Call actions endpoints removed from API reference

> POST /call-actions, GET /call-actions, GET /call-actions/{id}, PUT /call-actions/{id}, and DELETE /call-actions/{id} are no longer part of the customer API su

`POST /call-actions`, `GET /call-actions`, `GET /call-actions/{id}`, `PUT /call-actions/{id}`, and `DELETE /call-actions/{id}` are no longer part of the customer API surface and have been removed from the reference.

The routes remain in place for the dashboard, which uses cookie-session auth, but they were never reachable with a bearer API key. Any client that tried to call them with an API token received a 401. The API-ref section previously implied otherwise; that was incorrect.

Configure call-flow behavior through the agent config surface (`/agent/{id}` PATCH, and MCP `configure_call_actions` for `end_call` / `transfer_call`) instead.

## Call actions endpoints removed from API reference

> POST /call-actions, GET /call-actions, GET /call-actions/{id}, PUT /call-actions/{id}, and DELETE /call-actions/{id} are no longer part of the customer API su

`POST /call-actions`, `GET /call-actions`, `GET /call-actions/{id}`, `PUT /call-actions/{id}`, and `DELETE /call-actions/{id}` are no longer part of the customer API surface and have been removed from the reference.

The routes remain in place for the dashboard, which uses cookie-session auth, but they were never reachable with a bearer API key. Any client that tried to call them with an API token received a 401. The API-ref section previously implied otherwise; that was incorrect.

Configure call-flow behavior through the agent config surface (`/agent/{id}` PATCH, and MCP `configure_call_actions` for `end_call` / `transfer_call`) instead.

## PCA: aggregate limits on disposition metrics: combined prompt length and enum choices

> The Write to draft endpoint now enforces two aggregate limits when the payload includes postCallAnalyticsConfig.dispositionMetrics:

The [Write to draft](/api-reference/voice-agents/agent-versioning-branches/update-draft) endpoint now enforces two aggregate limits when the payload includes `postCallAnalyticsConfig.dispositionMetrics`:

* **Combined `dispositionMetricPrompt` length**. The sum across all metrics must be ≤ 50,000 characters. Exceeding it returns `400` with `The combined disposition metric prompts can be at most 50000 characters.`
* **ENUM choices per metric**. Each `dispositionMetricType: ENUM` metric can have at most 20 `choices`. Exceeding it returns `400` with `Each enum metric can have at most 20 choices. Ones already over the limit can stay as they are, but can't grow any further. Remove the extra choices to save.`

Both limits are grandfathered: agents already over either limit can still save as long as the change does not make it worse (total prompt length does not grow, and no over-limit enum grows or gains new choices). No cap on the number of metrics or on any single prompt's length.

Source: [atoms-platform#3448](https://github.com/smallest-inc/atoms-platform/pull/3448).

## PCA: aggregate limits on disposition metrics: combined prompt length and enum choices

> The Write to draft endpoint now enforces two aggregate limits when the payload includes postCallAnalyticsConfig.dispositionMetrics:

The [Write to draft](/api-reference/voice-agents/agent-versioning-branches/update-draft) endpoint now enforces two aggregate limits when the payload includes `postCallAnalyticsConfig.dispositionMetrics`:

* **Combined `dispositionMetricPrompt` length**. The sum across all metrics must be ≤ 50,000 characters. Exceeding it returns `400` with `The combined disposition metric prompts can be at most 50000 characters.`
* **ENUM choices per metric**. Each `dispositionMetricType: ENUM` metric can have at most 20 `choices`. Exceeding it returns `400` with `Each enum metric can have at most 20 choices. Ones already over the limit can stay as they are, but can't grow any further. Remove the extra choices to save.`

Both limits are grandfathered: agents already over either limit can still save as long as the change does not make it worse (total prompt length does not grow, and no over-limit enum grows or gains new choices). No cap on the number of metrics or on any single prompt's length.

Source: [atoms-platform#3448](https://github.com/smallest-inc/atoms-platform/pull/3448).

## Inbound SIP trunks: connection details for every trunk

> Every inbound SIP trunk row on the SIP Trunks dashboard page now has a Connection details dialog.

Every inbound SIP trunk row on the **SIP Trunks** dashboard page now has a **Connection details** dialog. It shows the **SIP origination host** to point your carrier at, **your numbers** as entered, and a ready-to-paste **example SIP URI** with the transport suffix (`;transport=tls` preferred, `;transport=tcp` fallback).

The same value is exposed in the API: `GET /sip-trunk/inbound` and `POST /sip-trunk/inbound` responses now include `sipHost` on every trunk. The host is derived at read time and is identical across your trunks.

## Recordings: authenticated presigned-URL flow

> New endpoint documented: GET /atoms/v1/recordings/{callId}?channel=mono|dual returns a short-lived presigned S3 URL for the call's audio (atoms-platform#3625, Phase 1 of PRO-2775)..

New endpoint documented: `GET /atoms/v1/recordings/{callId}?channel=mono|dual` returns a short-lived presigned S3 URL for the call's audio ([atoms-platform#3625](https://github.com/smallest-inc/atoms-platform/pull/3625), Phase 1 of PRO-2775). Presigned URLs expire in 15 minutes. Response envelope is `{ status: true, data: { url: "..." } }`.

Both channels are supported: `channel=mono` (default) for the composite track, `channel=dual` for stereo when the call was captured with per-side audio.

Prose docs updated:

* **[Accessing Recordings](/voice-agents/developer-guide/operate/analytics/call-metrics#accessing-recordings)** on Call Metrics now leads with the new endpoint, with cURL and Python examples plus a full error table (400 / 401 / 404).
* **Recording Review** on Monitoring and Debugging: the `get_recording` helper accepts a `channel` argument and hits the new endpoint.
* `recordingUrl` and `recordingDualUrl` field descriptions across the call detail response, the `post-conversation` webhook payload, and the campaign CSV export are now described as stable identifiers. Docs direct customers to resolve them through the new endpoint so integrations stay correct regardless of which cluster URL is returned.

The older endpoint `GET /atoms/v1/conversation/{callId}/recording/download-url` (mono only, `data.presignedUrl` envelope) stays documented as **legacy**; existing customers do not need to migrate immediately, but new integrations should use the recordings endpoint. Both endpoints share the same 15-minute presigned-URL TTL post-#3625.

Per-org rollout on the platform side: new organizations see the authenticated endpoint URL in `recordingUrl` fields; existing organizations continue to see raw CloudFront URLs today. The docs recommend always going through `/atoms/v1/recordings/{callId}` so integrations work either way and stay correct after the Phase 2 CloudFront lockdown lands.

## Telephony rework: SIP trunks as resources, agents answer, calls choose their caller ID

> The telephony model is reorganized around one idea: *who answers a number* is durable configuration, *what a call dials from* is a property of the call.

The telephony model is reorganized around one idea: *who answers a number* is durable configuration, *what a call dials from* is a property of the call. Full details and migration steps: [Telephony API migration guide](/api-reference/deprecations/telephony-migration).

**New**

* SIP trunks are first-class resources, one per direction: [`POST /sip-trunk/inbound`](/api-reference/voice-agents) and [`POST /sip-trunk/outbound`](/api-reference/voice-agents), with a dedicated **SIP Trunks** dashboard page.
* An agent answers on a number or trunk via `POST /agent/{agentId}/answers`; a source is answered by exactly one agent, and conflicts identify the agent holding it.
* Every call names its caller ID: `fromNumber` on outbound calls, `fromNumbers` (rotating, frozen at creation, retry-stable) on campaigns, and a per-agent transfer caller ID via `PUT /agent/{agentId}/transfer-source`.
* A temporary caller-ID bridge on the agent (`POST /agent/{agentId}/caller-ids`): shared attachments that outbound calls without a `fromNumber` dial from, first attached wins. Deprecated at introduction (responses carry `Deprecation: true`) and removed with the migration window; pass `fromNumber` per call instead.
* Guarded deletions everywhere: deleting a trunk still in use returns `409` naming what uses it, and releasing a number an agent answers on or dials from is refused.

**Changed**

* No more silent fallback to a platform-owned caller ID: a call that cannot resolve its from-number is refused with a `400` naming your options (dashboard test calls keep a labelled shared test number).
* Campaigns follow the same rule: a campaign whose `fromNumbers` (or legacy `phoneNumberIds`) resolve to zero numbers your organization owns is refused at start.
* The dashboard's "Import SIP" tab is replaced by the SIP Trunks page; the Phone Numbers page shows the **Answering agent** per number and blocks releasing a number an agent still answers on.

**Deprecated (still working for 45 days from this release)**

* `POST /product/import-phone-number`, `telephonyProductId` and `allowInboundCall` on agent writes, `fromProductId` on outbound calls, `phoneNumberIds` on campaigns, and the `agentId` / `customProducts` read shapes. Deprecated paths respond with a `Deprecation: true` header. The full list and the concrete sunset date live on the [Deprecation Notices](/api-reference/deprecations/voice-agents) page.

## New page: SIP Wire Reference

> New deep-dive page for SIP trunk integrators: SIP Wire Reference, linked from the SIP Trunking page.

New deep-dive page for SIP trunk integrators: [SIP Wire Reference](/voice-agents/telephony/phone-numbers/sip-wire-reference), linked from the SIP Trunking page.

It documents the exact wire contract, sourced from production packet captures: inbound and outbound call-flow ladders (including the digest-auth challenge on outbound), a real sanitised INVITE with its SDP, how each of your final responses is handled (what gets retried, what gets CANCELled), transfer semantics (a second outbound INVITE carrying `X-Caller-ID`, never SIP REFER, two channels per transferred call), the timer table (RTP keepalive, ringing, max duration), and how to debug with support using the `SCL_` call ID that rides on the `From` tag of every INVITE we send.

The SIP Trunking troubleshooting section now also tells you to include that call ID in support tickets: every call that reaches us has a full capture on our side, and the ID is how support pulls it.

## SIP Trunking: UDP is accepted; the real hazard is fragmentation

> Correction to the 2026-08-28 transport contract.

Correction to the [2026-08-28 transport contract](/voice-agents/telephony/phone-numbers/sip-trunking#transport-contract). The previous version stated our SIP ingress rejects UDP signalling outright. That was wrong: the ingress accepts UDP, TCP, and TLS, verified with a live OPTIONS probe over UDP answered with `200 OK`.

**What is actually true, and why the old advice still mostly worked.** Full-size INVITEs (SDP body, several codecs, custom headers) can exceed the size a UDP datagram survives on the path. Fragments are silently discarded by many networks and firewalls, so the INVITE never arrives: no reply, no ICMP, retransmissions time out. A small OPTIONS ping over the same UDP path succeeds, which is what makes the failure look mysterious. Switching signalling to TCP or TLS fixes it, which is why the earlier "use TCP/TLS" recommendation resolved real integrations even though the stated reason was wrong.

**Pages updated**: [SIP Trunking](/voice-agents/telephony/phone-numbers/sip-trunking) (transport table, troubleshooting, limitations), the Twilio / Telnyx / Vonage setup guides, [Phone Numbers](/voice-agents/telephony/phone-numbers) (Import SIP field table), and the FAQ. The recommendation is unchanged: prefer TLS (5061), fall back to TCP (5060), avoid UDP for signalling.

**Also corrected on the SIP Trunking page**: codecs offered on the SIP leg are G.722 and G.711 (PCMU / PCMA), not Opus; DTMF is RFC 2833/4733 `telephone-event` only (SIP INFO removed); added the RTP keepalive rule (calls end after 15 s without RTP mid-call, 30 s at setup), the early-offer requirement, `X-*` custom header passthrough into agent variables, and how call transfers appear on your trunk (a second outbound call, no SIP REFER, two channels per transferred call).

## SIP Trunking: UDP is accepted; the real hazard is fragmentation

> Correction to the 2026-08-28 transport contract.

Correction to the [2026-08-28 transport contract](/voice-agents/telephony/phone-numbers/sip-trunking#transport-contract). The previous version stated our SIP ingress rejects UDP signalling outright. That was wrong: the ingress accepts UDP, TCP, and TLS, verified with a live OPTIONS probe over UDP answered with `200 OK`.

**What is actually true, and why the old advice still mostly worked.** Full-size INVITEs (SDP body, several codecs, custom headers) can exceed the size a UDP datagram survives on the path. Fragments are silently discarded by many networks and firewalls, so the INVITE never arrives: no reply, no ICMP, retransmissions time out. A small OPTIONS ping over the same UDP path succeeds, which is what makes the failure look mysterious. Switching signalling to TCP or TLS fixes it, which is why the earlier "use TCP/TLS" recommendation resolved real integrations even though the stated reason was wrong.

**Pages updated**: [SIP Trunking](/voice-agents/telephony/phone-numbers/sip-trunking) (transport table, troubleshooting, limitations), the Twilio / Telnyx / Vonage setup guides, [Phone Numbers](/voice-agents/telephony/phone-numbers) (Import SIP field table), and the FAQ. The recommendation is unchanged: prefer TLS (5061), fall back to TCP (5060), avoid UDP for signalling.

**Also corrected on the SIP Trunking page**: codecs offered on the SIP leg are G.722 and G.711 (PCMU / PCMA), not Opus; DTMF is RFC 2833/4733 `telephone-event` only (SIP INFO removed); added the RTP keepalive rule (calls end after 15 s without RTP mid-call, 30 s at setup), the early-offer requirement, `X-*` custom header passthrough into agent variables, and how call transfers appear on your trunk (a second outbound call, no SIP REFER, two channels per transferred call).

## API reference: schema and enum corrections against backend Zod validators

> Atoms and Waves API reference updated to match what the backend actually enforces.

Atoms and Waves API reference updated to match what the backend actually enforces. Every change is Zod-derived and, where possible, live-verified against production.

## Atoms

* **`DELETE /agent/{id}/archive`.** `on` query flipped from optional `boolean` (default `true`) to required string enum `"true" | "false"`. This matches the Zod validator; passing a JSON boolean was already rejected, the previous docs were misleading. See the endpoint description for the exact accepted values.
* **`GET /conversation/{callId}/recording/download-url`.** Added the callId regex pattern `^CALL-\d{13}-[a-fA-F0-9]{6}$` so the SDK types it correctly and clients see it in the API-ref.
* **`POST /conversation/{callId}/cancel`.** Added `minLength: 1` on the path parameter (matches Zod).
* **`POST /campaign`.** `name` gains `maxLength: 40`.
* **`POST /knowledgebase`.** `description` gains `maxLength: 150`.
* **`POST /audience`.** `name` gains `maxLength: 80`.
* **`PUT /account/update-org-name`.** `name` minLength grew from 1 to 2, gains `maxLength: 50`.
* **`GET /product/get-available-numbers` and `POST /product/rent-number`.** `provider` enum extended to `[twilio, plivo, custom, whatsapp]` (was `[plivo, twilio]`) on both query and response.
* **`POST /product/import-phone-number`.** Adds `cpsLimit` (number, `minimum: 1`, `default: 1`, capped server-side by `CUSTOM_NUMBER_CPS_CEILING`). Every imported number is always paced.
* **`slmModel` and the three `CreateAgentRequest` LLM enums.** Extended to include `gpt-5.2-azure` alongside `gpt-5.2` for Azure-hosted access.
* **`WorkflowType` enum.** Adds `multi_agents` alongside the existing `workflow_graph` and `single_prompt`. `multi_agents` is restricted; the server requires `slmModel` to be one of `[electron, gpt-5.2-azure]`.

## Waves

* **`GET /waves/v1/{model}/get_voices`.** Removes `bearerAuth` requirement. This endpoint is public per `optionalAuthMiddleware`; live-verified as `HTTP/2 200` without an `Authorization` header. SDK-generated client bindings should mark this method as no-auth-required.
* **`GET /waves/v1/voice/get-all-models`.** Auth is now optional (`{}` union with `bearerAuth`). Both anonymous and authenticated calls are accepted, matching the same middleware behavior.
* **`POST /waves/v1/pulse/get_text`.** Documents four backend query params that were missing from the spec: `webhook_method` (`POST | GET`, default `POST`), `redact_pii`, `redact_pci` (both `"true" | "false"`, English/Hindi only), and `numerals` (`"true" | "false" | auto`, default `auto`).
* **`POST /waves/v1/tts`.** Response body content-type mapping is now correct per format (`wav → audio/wav`, `mp3 → audio/mpeg`, `ulaw`/`alaw` → `audio/basic`, `pcm → application/octet-stream`); already documented in a prior changelog entry, no change required here.
* **`POST /waves/v1/voice-cloning`.** `language` field gains the explicit 22-value enum matching `LIGHTNING_V3_1_LANGUAGES`. Also drops the retired `/waves/v1/lightning-large/add_voice` reference from the endpoint description.
* **PCA (Post-Call Analytics).** Three response fields added to match the backend contract.
* **Waves Analytics.** Response schemas expanded to cover the new dashboard metric surfaces.

## Path parameter naming

The backend's Zod validators destructure path params under different names on some endpoints (`req.params.id`, `req.params.conversationId`). This audit intentionally does not rename the OpenAPI placeholders. Path parameter names in OpenAPI are docs-only: the URL is `/conversation/CALL-XXX/cancel` regardless of whether the spec uses `{callId}`, `{id}`, or `{conversationId}`. Keeping the docs-facing name `callId` consistent across `/conversation/{callId}/recording/download-url`, `/conversation/{callId}/cancel`, and every other conversation-scoped endpoint is safer for readers and avoids a breaking SDK method-argument rename with zero customer-observable benefit.

## Realtime Agent WebSocket: per-direction audio format contract

> The realtime voice-agent WebSocket API now has a documented contract for the audio you send and the audio the agent sends back.

The realtime voice-agent WebSocket API now has a documented contract for the audio you send and the audio the agent sends back. Previously the only knob was `sample_rate`, and it silently set only the output; input audio was read at a hardcoded rate regardless of what the client asked for. That mismatch was invisible: the call worked, the transcription was wrong.

Two new fields on [`POST /conversation/register-call`](/api-reference/voice-agents/realtime-agent/register-call) and on the WebSocket connect URL:

* **`input_audio_format`** — what you send. `pcm_{8000,16000,22050,24000,44100,48000}`, `mulaw_8000`, `alaw_8000`, or `opus_{8000,16000,24000,48000}`.
* **`output_audio_format`** — what the agent sends back. `pcm_{8000,16000,24000}` on every voice, plus `pcm_44100` on `lightning-v3.1` and `lightning-v3.1-pro`.

One `<encoding>_<rate>` token per direction. `sample_rate` is deprecated in favor of `output_audio_format` and stays accepted forever; existing integrations do not need to change. Sending both is fine when they agree; a disagreement is refused with HTTP 400 rather than one silently winning.

An unsupported input token is refused at register-call with the list of accepted values in the error. An `output_audio_format` the agent's voice cannot render is refused there too, before any session exists, rather than failing mid-call.

The accepted tokens and error bodies are on [Register Call](/api-reference/voice-agents/realtime-agent/register-call) and [Agent WebSocket](/api-reference/voice-agents/realtime-agent/realtime-agent). Opus framing, the voice-dependent output rate, and migrating off `sample_rate` are on the new [Audio Formats](/voice-agents/integrate/audio-formats) page.

## Realtime Agent WebSocket: per-direction audio format contract

> The realtime voice-agent WebSocket API now has a documented contract for the audio you send and the audio the agent sends back.

The realtime voice-agent WebSocket API now has a documented contract for the audio you send and the audio the agent sends back. Previously the only knob was `sample_rate`, and it silently set only the output; input audio was read at a hardcoded rate regardless of what the client asked for. That mismatch was invisible: the call worked, the transcription was wrong.

Two new fields on [`POST /conversation/register-call`](/api-reference/voice-agents/realtime-agent/register-call) and on the WebSocket connect URL:

* **`input_audio_format`** — what you send. `pcm_{8000,16000,22050,24000,44100,48000}`, `mulaw_8000`, `alaw_8000`, or `opus_{8000,16000,24000,48000}`.
* **`output_audio_format`** — what the agent sends back. `pcm_{8000,16000,24000}` on every voice, plus `pcm_44100` on `lightning-v3.1` and `lightning-v3.1-pro`.

One `<encoding>_<rate>` token per direction. `sample_rate` is deprecated in favor of `output_audio_format` and stays accepted forever; existing integrations do not need to change. Sending both is fine when they agree; a disagreement is refused with HTTP 400 rather than one silently winning.

An unsupported input token is refused at register-call with the list of accepted values in the error. An `output_audio_format` the agent's voice cannot render is refused there too, before any session exists, rather than failing mid-call.

The accepted tokens and error bodies are on [Register Call](/api-reference/voice-agents/realtime-agent/register-call) and [Agent WebSocket](/api-reference/voice-agents/realtime-agent/realtime-agent). Opus framing, the voice-dependent output rate, and migrating off `sample_rate` are on the new [Audio Formats](/voice-agents/integrate/audio-formats) page.

## Realtime Agent WebSocket: per-direction audio format contract

> The realtime voice-agent WebSocket API now has a documented contract for the audio you send and the audio the agent sends back.

The realtime voice-agent WebSocket API now has a documented contract for the audio you send and the audio the agent sends back. Previously the only knob was `sample_rate`, and it silently set only the output; input audio was read at a hardcoded rate regardless of what the client asked for. That mismatch was invisible: the call worked, the transcription was wrong.

Two new fields on [`POST /conversation/register-call`](/api-reference/voice-agents/realtime-agent/register-call) and on the WebSocket connect URL:

* **`input_audio_format`** — what you send. `pcm_{8000,16000,22050,24000,44100,48000}`, `mulaw_8000`, `alaw_8000`, or `opus_{8000,16000,24000,48000}`.
* **`output_audio_format`** — what the agent sends back. `pcm_{8000,16000,24000}` on every voice, plus `pcm_44100` on `lightning-v3.1` and `lightning-v3.1-pro`.

One `<encoding>_<rate>` token per direction. `sample_rate` is deprecated in favor of `output_audio_format` and stays accepted forever; existing integrations do not need to change. Sending both is fine when they agree; a disagreement is refused with HTTP 400 rather than one silently winning.

An unsupported input token is refused at register-call with the list of accepted values in the error. An `output_audio_format` the agent's voice cannot render is refused there too, before any session exists, rather than failing mid-call.

The accepted tokens and error bodies are on [Register Call](/api-reference/voice-agents/realtime-agent/register-call) and [Agent WebSocket](/api-reference/voice-agents/realtime-agent/realtime-agent). Opus framing, the voice-dependent output rate, and migrating off `sample_rate` are on the new [Audio Formats](/voice-agents/integrate/audio-formats) page.

## Voice Agents API reference: Agents-first ordering, widget endpoints deprecated

> The API reference sidebar now opens with Agents at the top, followed by Calls, Campaigns, Phone Numbers, and Knowledge Base.

The API reference sidebar now opens with **Agents** at the top, followed by **Calls**, **Campaigns**, **Phone Numbers**, and **Knowledge Base**. User, Account, and Billing sit at the bottom. Matches the ordering pattern used by every peer voice-AI reference (Vapi, Retell, Bland, Deepgram Voice Agent, ElevenLabs Conversational AI). No URL slugs changed; only the sidebar ordering.

Three widget-only endpoints are marked deprecated and hidden from the sidebar:

* `GET /agent/{id}/widget-config`
* `PATCH /agent/{id}/widget-config`
* `POST /agent/{id}/avatar/presigned-url`

These control the embeddable chat widget and its avatar image. The endpoints still return `200` for existing integrations, but they are not part of the customer-facing REST surface. Configure the widget from the dashboard instead. Existing URLs still resolve (`/api-reference/voice-agents/agents/get-agent-widget-config`, `update-agent-widget-config`, `get-agent-avatar-presigned-url`).

Listed on the [Deprecation Notices](/api-reference/deprecations/voice-agents#widget) page. The `widgetConfig` object on the agent response body is unchanged (safe to ignore for API-only integrations).

## Voice Agents: Account, Web Call, and Campaign export endpoints on the API reference

> Six previously undocumented Voice Agents endpoints now render on the API reference:

Six previously undocumented Voice Agents endpoints now render on the API reference:

**Account**

* `GET /account/get-account-details`: user profile plus the orgs the user belongs to
* `PUT /account/update-org-name`: rename the org scoped by the API key (owner role)

**Web Call**

* `POST /conversation/chat`: mints a short-lived session access token + room for a browser text-first session
* `POST /conversation/webcall`: same shape, voice-first session

**Campaigns**

* `GET /campaign/{id}/export/by-audience-member`: CSV, one row per contact with every attempt
* `GET /campaign/{id}/logs/export`: CSV, one row per call attempt

All routes have been in production. This change only documents them; no behavior changed.

## Voice Agents: Account, Web Call, and Campaign export endpoints on the API reference

> Six previously undocumented Voice Agents endpoints now render on the API reference:

Six previously undocumented Voice Agents endpoints now render on the API reference:

**Account**

* `GET /account/get-account-details`: user profile plus the orgs the user belongs to
* `PUT /account/update-org-name`: rename the org scoped by the API key (owner role)

**Web Call**

* `POST /conversation/chat`: mints a short-lived session access token + room for a browser text-first session
* `POST /conversation/webcall`: same shape, voice-first session

**Campaigns**

* `GET /campaign/{id}/export/by-audience-member`: CSV, one row per contact with every attempt
* `GET /campaign/{id}/logs/export`: CSV, one row per call attempt

All routes have been in production. This change only documents them; no behavior changed.

## SDK 5.5.0: typed errors, waves helpers, CLI expansion, crew custom-LLM gotcha, warm/cold transfer clarified

> Docs updates for the smallestai 5.5.0 release, plus the sibling platform fix for crew transfer audio.

Docs updates for the `smallestai 5.5.0` release, plus the sibling platform fix for crew transfer audio.

**Typed errors with actionable hints.** Plan or entitlement-gated requests (HTTP 400 with "…upgrade to a higher plan…") now raise `PlanNotEntitledError` (subclass of `BadRequestError`), importable from `smallestai` or the new `smallestai.errors` module. Error messages carry an actionable hint: `401` points at `SMALLEST_API_KEY`, plan-gated `400` points at upgrading, org-gated `403` points at the account team.

**Waves helpers.** `smallestai.waves.helpers` ships `synthesize_to_file`, `synthesize_bytes`, and `synthesize_with_expiry` for the common one-shot TTS shapes. Setting `expire_content=True` on the helpers sends the Enterprise-only `x-expire-content` header; `synthesize_with_expiry` also reads back the `x-content-expiry` outcome so callers can log or surface it.

**CLI reference expanded.** `client-libraries/overview.mdx` now lists all seven `smallestai` command groups: `auth`, `agents`, `agent-crew`, `calls`, and the three added in 5.4.2 (`waves`, `campaigns`, `phone-numbers`). Billable and destructive subcommands prompt for confirmation unless `--yes` is passed.

**Custom-LLM crew gotcha.** The BYOM page now warns against pointing a crew's `OpenAIClient` at Anthropic's OpenAI-compatible endpoint (`https://api.anthropic.com/v1/`). That URL truncates streaming replies and drops tool-call turns, which surfaces as the agent going silent mid-conversation. For Claude on a crew, use a gateway that stabilizes the stream (LiteLLM, OpenRouter, Portkey), or the platform model. The page also gets a `ngrok` note (ERR\_NGROK\_8012 when the local process is down surfaces as a fatal `agent_error` and hangs up) and a `tool_choice` note (crew `chat()` forwards extra kwargs, so `tool_choice="required"` works for deterministic transfer decisions).

**Call Transfer guide.** The cold-vs-warm section on `dev/build/phone-calling/call-features/call-transfer.mdx` now has separate subsections for each method, spells out that warm is a private briefing (with the "please hold" announcement on the caller side and the whisper on the destination side), and lists the three audio slots warm supports (`on_hold_music`, whisper, three-way). Cold stays a direct connect with no audio window. Also adds two new Troubleshooting entries from customer debug: transfer fires but the far end rejects in \~2s (destination not answering), and the agent says "let me transfer you" without ever firing the tool (LLM determinism; prompt fix + `tool_choice="required"`).

**Cookbook cross-link.** The `voice-agents/call_transfer` [sample](https://github.com/smallest-inc/cookbook/tree/main/voice-agents/call_transfer) covers cold, warm, inbound, and agent-to-agent shapes and now ships an `AGENTS.md` so a coding agent can consume it directly.

_Showing the 20 most recent of 60 entries. Append `/llms.txt` to the changelog URL for the complete index._