API: text screened before synthesis; native client tool protocol, conversation identity, injection guardrail, slow-tool filler, SIP-header variables
API: text screened before synthesis; native client tool protocol, conversation identity, injection guardrail, slow-tool filler, SIP-header variables
Text you send for synthesis is now screened before Speechify produces audio from it. A request whose content is not permitted returns 400 content_policy_violation with no audio, and is not billed. This applies to /v1/audio/speech, /v1/audio/stream and /v1/audio/stream/with-timestamps, and to an agent’s prompt and first_message when you create or update one.
Most published work is unaffected - fiction, journalism, true crime and court reporting routinely describe or quote violence, and depicting that material is treated differently from producing it.
One thing worth handling in your client: content_policy_violation is a persistent error, so retrying the same text will be refused again. On the streaming endpoints the decision is always made before the first audio byte, so a 200 means the request passed and a refusal is always a JSON 400, never a truncated or empty audio file. See Content Policy.
Native client tool protocol v1
Client tools now have a versioned wire protocol (contracts/realtime/), so a native LiveKit client and the Speechify web widget implement one contract instead of two readings of an undocumented channel. Requests and responses travel on LiveKit’s reliable data channel under the reserved simba.tools topic.
- Agent → client:
{ "protocol_version": 1, "type": "tool_request", "id", "name", "arguments" }. - Client → agent:
{ "protocol_version": 1, "type": "tool_response", "id", … }, carrying the sameidand exactly one ofresultorerror. - A structured
errorcarries acode— one ofinvalid_request,unsupported_protocol,tool_not_registered,tool_execution_failed,payload_too_large— and amessage. The legacy string form"error": "message"is still accepted. protocol_versionis optional on read and defaults to1; new clients should always write it.- Each UTF-8 JSON packet is capped at 14 KiB (14336 bytes), enforced in both directions.
If you built a client tool from the previous documentation, it never worked. The old guide showed a tool_call / tool_call_id shape that neither the widget nor the worker has ever accepted — a client written to it could not receive a call. The guide is corrected: the request type is tool_request and the correlation field is id. See Add tools.
Conversation identity and per-conversation voice override
POST /v1/agents/{agent_id}/conversations (Create Conversation) gains two optional fields:
user_identity— a stable, opaque end-user key from your application. It is persisted verbatim as the conversation’scaller_identityand is the key the post-call memory extractor pivots memories by. Omit it for an anonymous session, which is excluded from per-caller memory. It may not begin withembed_,anon_oruser_(namespaces the platform derives itself); claiming one is rejected400naminguser_identity.overrides.voice_id— a voice fromGET /v1/agents/voicesthat replaces the agent’s configured voice for the whole conversation, including every configured language. The override is scoped to this conversation; if the call transfers to another agent, that agent speaks with its own configured voice. A voice that is unavailable or cannot cover the agent’s configured language set is rejected — the error envelope’sfieldsmap namesoverrides.voice_id, and an unavailable voice is never silently swapped for the default.
caller_identity is redefined. It was the LiveKit realtime participant identity; it is now an application-level caller key, independent of the opaque per-session participant id. For an authenticated create it is user_identity when supplied and otherwise falls back to user_<authenticated principal>. See Voice and language.
Out-of-band injection guardrail
A new agent config block guardrails.observer_enabled (boolean, default false) arms an out-of-band safety observer. A second, isolated model reads the caller’s turns off the live conversation and corrects the agent when it detects a privileged-identity claim, a prompt-extraction probe, an instruction override, or the caller pulling the call off its purpose. It runs off the turn critical path, so it adds no caller-facing latency, and it never receives the agent’s own instructions — so nothing the caller says can talk it out of running.
It bills up to one extra inference call per caller turn. That is why it is opt-in per agent rather than a platform default. guardrails is now a required field on the Agent response object; observer_enabled defaults to false. The conversation agent_snapshot records observer_guardrail_enabled — whether the check was armed when the call started.
Keep the caller audible while a slow tool runs
A webhook or MCP tool can now declare itself slow with a long_running block (LongRunningToolConfig), so the agent speaks a holding phrase instead of leaving the caller in dead air, and refuses to fire the same tool twice while one call is in flight. It is opt-in per tool — a tool without the block is byte-identical on the wire and keeps blocking silently.
filler_message(1–200 chars) — the phrase spoken verbatim once the tool has run forfiller_delay_ms. Write it in the agent’s own language and voice; Speechify ships no default phrase. Omit it to arm the duplicate policy with no spoken filler.filler_delay_ms(500–30000, default 2000) — how long the tool must have been running, with nobody speaking, before the filler is spoken. The wait restarts whenever anyone speaks, so the filler never lands on live speech.on_duplicate(reject|allow, defaultreject) —rejectrefuses a second call to the same tool while the first is in flight, so a slow side-effecting webhook cannot be fired twice;allowrestores the unguarded behaviour for a read-only tool.
The tool still blocks the turn: the agent never receives a synthetic result, so it can only ever speak about a result it actually got. long_running cannot be combined with fire_and_forget — that path returns before your endpoint answers, so there is no wait to fill, and saving both is refused.
Map inbound SIP headers to agent variables
Create Trunk gains headers_to_attributes: a map from inbound SIP INVITE extension headers onto agent variables, so an upstream that already identified the caller can pass that through instead of the agent re-verifying it in conversation. A header mapped to customer_id renders in the agent’s prompt and in flow tool arguments as {{system__sip_customer_id}}.
- Keys are
X-…extension headers (a valid SIP token); values name the variable (^[a-z][a-z0-9_]{0,39}$); up tomaxProperties: 8. - Two headers may not map to the same variable name — the server rejects it with
validation_failed. - Inbound calls only. For
provider=byocthe mapping takes effect when the trunk’s first phone number is imported (when its inbound route is provisioned).
A mapped header is an identity assertion from your own phone system and is only as trustworthy as that system — treat it as a hint, not proof, and values reach the agent as data, never as instructions. The Trunk response echoes the configured headers_to_attributes. See Add a phone number.