{"templateId":"markdown","sharedDataIds":{"sidebar":"sidebar-docs/voice-2.0/sidebars.yaml"},"props":{"metadata":{"markdoc":{"tagList":[]},"product":"Voice v2 (preview)","type":"markdown"},"seo":{"title":"Connect an AI Chatbot (Voice Relay)","siteUrl":"https://developers.sinch.com","llmstxt":{"title":"Sinch Developer Documentation","description":"LLMs.txt containing a map of all the documentation files for Sinch.","sections":[{"title":"Numbers API","description":"The Numbers API enables you to search for, view, and activate numbers. It's considered a precursor to other APIs in the Sinch product family. The numbers API can be used in tandem with any of our APIs that perform messaging or calling.","includeFiles":["docs/numbers/**/*.md","docs/numbers/**/*.yaml"],"excludeFiles":["docs/numbers/index.md"]},{"title":"Conversation API","description":"Send and receive messages globally on many popular channels with ease and confidence when using Sinch's Conversation API. Conversation API is the preferred API for sending mobile messages on SMS and other social channels with Sinch. It is a simple API with unified error messages, consistent request payloads, and common webhook payloads that are channel-agnostic.","includeFiles":["docs/conversation/**/*.md","docs/conversation/**/*.yaml"],"excludeFiles":["docs/conversation/index.md"]},{"title":"Voice API","description":"The Voice API works as a big telephony switch. The Voice API handles incoming phone calls (also known as incoming call “legs”), sets up outgoing phone calls (or outgoing call “legs”), and bridges the two. The incoming call leg may come in over a data connection (from a smartphone or web application using the Sinch SDKs) or through a local phone number (from the PSTN network). Similarly, the outgoing call leg can be over data (to another smartphone or web application using the Sinch SDKs) or the PSTN network.","includeFiles":["docs/voice/**/*.md","docs/voice/**/*.yaml"],"excludeFiles":["docs/voice/index.md"]},{"title":"Voice API v2","description":"The Voice API works as a big telephony switch. The Voice API handles incoming phone calls (also known as incoming call “legs”), sets up outgoing phone calls (or outgoing call “legs”), and bridges the two. The incoming call leg may come in over a data connection (from a smartphone or web application using the Sinch SDKs) or through a local phone number (from the PSTN network). Similarly, the outgoing call leg can be over data (to another smartphone or web application using the Sinch SDKs) or the PSTN network.","includeFiles":["docs/voice-2.0/**/*.md","docs/voice-2.0/**/*.yaml"],"excludeFiles":["docs/voice-2.0/index.md"]},{"title":"Verification API","description":"The Verification API is a platform for phone number verification. It consists of the API and different software development kits (the Sinch SDKs) that you integrate with your smartphone or web application and cloud based back-end services. Together they enable SMS, Flashcall, Phone Call and Data verification in your application.","includeFiles":["docs/verification/**/*.md","docs/verification/**/*.yaml"],"excludeFiles":["docs/verification/index.md"]},{"title":"Provisioning API","description":"Provisioning API allows you to programmatically set up your senders, accounts and templates on your favorite messaging platforms on the Conversation API. For now, you can create your first WhatsApp channel through Meta's Embedded sign up, you can configure your first SMS App and configure your webhooks. As development continues, we will be adding the most commonly used channels.","includeFiles":["docs/provisioning-api/**/*.md","docs/provisioning-api/**/*.json"],"excludeFiles":["docs/provisioning-api/index.md"]},{"title":"Elastic SIP Trunking API","description":"With Elastic SIP Trunking you can create and manage your SIP trunks and phone numbers programmatically.","includeFiles":["docs/est/**/*.md","docs/est/**/*.yaml"],"excludeFiles":["docs/est/index.md"]},{"title":"Fax API","description":"Send and receive HIPAA compliant faxes on our modern fax platform using our developer-friendly API.","includeFiles":["docs/fax/**/*.md","docs/fax/**/*.yaml"],"excludeFiles":["docs/fax/index.md"]},{"title":"In-app Voice and Video SDK","description":"The In-app Voice and Video SDK enables you to add voice and video calling capabilities directly into your mobile or web application using the Sinch SDKs.","includeFiles":["docs/in-app-calling/**/*.md"],"excludeFiles":["docs/in-app-calling/index.md"]},{"title":"Number Lookup API","description":"The Number Lookup API is designed to provide in-depth information about phone numbers, helping enterprises enhance their communication strategies and prevent fraud. By identifying the type of phone line (for example, mobile, landline, VoIP) and the associated carrier, the API allows businesses to optimize routing, reduce unnecessary costs, and improve customer engagement.","includeFiles":["docs/number-lookup-api-v2/**/*.md","docs/number-lookup-api-v2/**/*.yaml"],"excludeFiles":["docs/number-lookup-api-v2/index.md"]},{"title":"Functions","description":"Serverless compute for voice and messaging. Deploy your code and Sinch routes live calls and messages to it — no infrastructure to run.","includeFiles":["docs/functions/functions/**/*.md","docs/functions/concepts/**/*.md","docs/functions/reference/**/*.md"],"excludeFiles":["docs/functions/functions/index.md"]},{"title":"CLI","description":"One command line for every Sinch API and the full Functions lifecycle — scaffold, run locally, deploy, and manage.","includeFiles":["docs/functions/cli/**/*.md"],"excludeFiles":["docs/functions/cli/index.md"]}]}},"dynamicMarkdocComponents":[],"compilationErrors":[],"ast":{"$$mdtype":"Tag","name":"article","attributes":{},"children":[{"$$mdtype":"Tag","name":"Heading","attributes":{"level":1,"id":"connect-an-ai-chatbot-voice-relay","__idx":0},"children":["Connect an AI Chatbot (Voice Relay)"]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"overview","__idx":1},"children":["Overview"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The Sinch Voice API's ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Voice Relay"]}," destination type connects a live phone call to a WebSocket server that speaks a simple text-in / text-out protocol. Unlike the raw PCM streaming in ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/docs/voice-2.0/tutorials/stream-audio"},"children":["Stream Call Audio"]},", Voice Relay performs speech-to-text and text-to-speech ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["on the Sinch side"]},", so your server only sends and receives plain text. Wiring a language model into a real phone call is therefore mostly a matter of forwarding text turns to your LLM provider."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["This tutorial uses a Python WebSocket server backed by ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"https://www.langchain.com/"},"children":["LangChain"]},", so the same code works with OpenAI, Anthropic Claude, or Google Gemini by changing one environment variable."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"real-life-examples","__idx":2},"children":["Real-life examples"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["AI receptionist:"]}," answer inbound calls 24/7, handle FAQs, route or escalate based on the conversation."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Product demo bot:"]}," let prospects call a number and talk to an AI that explains your product."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Internal helpdesk:"]}," staff call a number to ask an LLM about internal tools, runbooks, or HR policies."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Voice-first prototyping:"]}," validate a conversational AI design over a real phone call before building a full IVR."]}]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"get-the-code","__idx":3},"children":["Get the code"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["All files for this tutorial live in the ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"https://github.com/sinch/sinch-voice-tutorials"},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sinch/sinch-voice-tutorials"]}]}," repository, under ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["4.1-voice-relay"]},". Clone it first:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"git clone https://github.com/sinch/sinch-voice-tutorials.git\ncd sinch-voice-tutorials\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Every path in this guide is relative to the repo. The tutorial folder has its ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["own"]}," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":[".env.example"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["requirements.txt"]},", separate from the tutorials-root ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":[".env"]},"."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"prerequisites","__idx":4},"children":["Prerequisites"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["A ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"https://dashboard.sinch.com"},"children":["Sinch account"]}," with a project, API credentials, and a virtual phone number."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Python 3.10 or newer."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["An API key for one LLM provider: OpenAI, Anthropic Claude, or Google Gemini."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"https://ngrok.com"},"children":["ngrok"]}," (or another way to expose a local port over ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["wss://"]},") for development."]}]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"fastest-path-to-first-success","__idx":5},"children":["Fastest path to first success"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["You will: install deps → set one LLM key → expose the server with ngrok → point a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["VOICE_RELAY"]}," destination at it → call the number and talk to the bot."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"1-install-dependencies","__idx":6},"children":["1. Install dependencies"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"cd tutorials/4.1-voice-relay\npython -m venv .venv && source .venv/bin/activate   # optional but recommended\npip install -r requirements.txt\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["requirements.txt"]}," pulls in ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["websockets"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["python-dotenv"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["langchain-core"]},", and one provider package per LLM (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["langchain-openai"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["langchain-anthropic"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["langchain-google-genai"]},"). You only need the package for the provider you choose, but installing all three is harmless."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"2-set-your-llm-key","__idx":7},"children":["2. Set your LLM key"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"cp .env.example .env\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Edit ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":[".env"]},":"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"ini","header":{"controls":{"copy":{}}},"source":"# Provider: openai | claude | gemini\nPROVIDER=claude\nAPI_KEY=sk-ant-...\n","lang":"ini"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["API_KEY"]}," is the key for whichever ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["PROVIDER"]}," you pick. Where to get one:"]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"PROVIDER"},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["PROVIDER"]}]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Get a key at"},"children":["Get a key at"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Default model"},"children":["Default model"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["openai"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"https://platform.openai.com/api-keys"},"children":["https://platform.openai.com/api-keys"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["gpt-4o"]}]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["claude"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"https://console.anthropic.com/settings/keys"},"children":["https://console.anthropic.com/settings/keys"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["claude-haiku-4-5"]}]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["gemini"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"https://aistudio.google.com/apikey"},"children":["https://aistudio.google.com/apikey"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["gemini-2.0-flash"]}]}]}]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Optional overrides: ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["MODEL"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["TEMPERATURE"]}," (default ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["0.7"]},"), ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["MAX_TOKENS"]}," (default ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["1024"]},"), ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["GREETING"]}," (default ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Hello!"]},"), ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["PORT"]}," (default ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["8765"]},")."]},{"$$mdtype":"Tag","name":"blockquote","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Early local check (before any phone call)."]}," You can confirm the server starts and the LLM key works without placing a call:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"python server.py\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["On startup it prints the provider, model, max tokens, and that it loaded ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["system_prompt.md"]},". If the key is bad you will see an authentication error from the provider as soon as the first turn runs. Stop here and fix ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":[".env"]}," before going further, since a broken key produces a silent, unresponsive bot on the call that is much harder to debug."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"3-start-the-server-and-expose-it-with-ngrok","__idx":8},"children":["3. Start the server and expose it with ngrok"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["In one terminal:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"python server.py\n# [*] Provider: claude  Model: claude-haiku-4-5  Temp: 0.7  MaxTokens: 1024\n# [*] Agent Relay listening on ws://0.0.0.0:8765\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["In a second terminal:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"ngrok http 8765\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Copy the ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Forwarding"]}," URL (e.g. ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["https://abc123.ngrok-free.app"]},"). For the Voice Relay endpoint you must use the ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["wss://"]}]}," form of it: ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["wss://abc123.ngrok-free.app"]},"."]},{"$$mdtype":"Tag","name":"blockquote","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["About ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["WS_ENDPOINT"]}," / ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["WS_URL"]},"."]}," The tutorials-root ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":[".env.example"]}," defines a shared ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["WS_ENDPOINT"]}," variable, but ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["server.py"]}," does not read it"]},". The relay server only reads ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["PROVIDER"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["API_KEY"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["MODEL"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["TEMPERATURE"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["MAX_TOKENS"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["GREETING"]},", and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["PORT"]},". The ngrok ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["wss://"]}," URL is configured on the Sinch side (in the SVAML ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["voiceRelay.endpoint"]},", below), not in any ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":[".env"]},". There is no ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["WS_URL"]}," variable anywhere in this tutorial; ignore both names for the relay server itself."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"4-point-a-voice_relay-destination-at-your-server","__idx":9},"children":["4. Point a VOICE_RELAY destination at your server"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["A Voice Relay destination is a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dial"]}," target of type ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["VOICE_RELAY"]},". The minimal destination object is:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"{\n  \"type\": \"VOICE_RELAY\",\n  \"voiceRelay\": {\n    \"endpoint\": \"wss://abc123.ngrok-free.app\",\n    \"ttsVoice\": \"Emma\",\n    \"sttLanguage\": \"en-US\"\n  }\n}\n","lang":"json"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Verified against the OpenAPI spec (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["voiceRelay"]}," schema): ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["endpoint"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ttsVoice"]},", and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sttLanguage"]}," are ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["required"]},". ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sttLanguage"]}," is a BCP-47 tag (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["en-US"]},"). Optional fields are ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["enableInterruptions"]}," (boolean, default ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["true"]},") and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callHeaders[]"]}," (up to 16 key/value objects, each value/key ≤ 255 chars)."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The simplest way to make every inbound call reach the bot is a ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["STATIC"]}," call behavior on your Sinch service: a fixed SVAML script the platform runs for every inbound call, with no webhook server of your own. Configure it on the ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"https://dashboard.sinch.com/voice/services"},"children":["dashboard"]}," or via the API:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"curl -X PATCH \\\n  -u \"$KEY_ID:$KEY_SECRET\" \\\n  \"https://voice.api.sinch.com/v2/projects/$PROJECT_ID/services/$SERVICE_ID\" \\\n  -H \"Content-Type: application/json\" \\\n  -d @- <<'JSON'\n{\n  \"callBehavior\": {\n    \"type\": \"STATIC\",\n    \"static\": {\n      \"callName\": \"caller\",\n      \"commands\": [\n        { \"command\": \"answer\" },\n        { \"command\": \"bridgeCall\", \"bridgeName\": \"main-bridge\" },\n        {\n          \"command\": \"dial\",\n          \"callName\": \"voice_relay_call\",\n          \"to\": {\n            \"type\": \"VOICE_RELAY\",\n            \"voiceRelay\": {\n              \"endpoint\":    \"wss://abc123.ngrok-free.app\",\n              \"ttsVoice\":    \"Emma\",\n              \"sttLanguage\": \"en-US\"\n            }\n          },\n          \"events\": {\n            \"onAnswer\": [\n              { \"command\": \"bridgeCall\", \"bridgeName\": \"main-bridge\" }\n            ]\n          }\n        }\n      ],\n      \"events\": {\n        \"onHangup\": [\n          { \"command\": \"hangup\", \"callName\": \"voice_relay_call\" }\n        ]\n      }\n    }\n  }\n}\nJSON\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Auth is ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["HTTP Basic"]}," with your project key (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["KEY_ID:KEY_SECRET"]},"). Set ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["KEY_ID"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["KEY_SECRET"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["PROJECT_ID"]},", and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["SERVICE_ID"]}," from the tutorials-root ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":[".env"]}," (or your shell). Replace ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["endpoint"]}," with your current ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["wss://"]}," ngrok URL. It changes every time you restart ngrok on the free tier."]},{"$$mdtype":"Tag","name":"blockquote","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ttsVoice"]}," value."]}," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Emma"]}," is the example voice from the spec; ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Tiffany"]}," (shipped in ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["static.txt"]},") is another. For the full list of supported voices see the ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"https://developer.sinch.com/docs/voice/api-reference/text-to-speech-voices"},"children":["Text-to-Speech Voices reference"]},". If a voice name is wrong the platform falls back or errors at call time, so verify the name against that list."]}]},{"$$mdtype":"Tag","name":"blockquote","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Spec note: ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["static.events"]},"."]}," The OpenAPI ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["staticCallBehavior.events"]}," schema documents only ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onHangup"]}," at the top level (used above). The per-leg ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dial.events"]}," block supports the full call-event set (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onAnswer"]},", etc.)."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"5-call-the-number-and-talk-to-the-bot","__idx":10},"children":["5. Call the number and talk to the bot"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Dial your Sinch virtual number from any phone. The flow:"]},{"$$mdtype":"Tag","name":"ol","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["The platform answers, opens a WebSocket to your ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["wss://"]}," endpoint, and sends a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["connect"]}," message."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Your server replies ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["answer"]}," and an optional greeting, which Sinch's TTS speaks to you."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["You talk; Sinch transcribes your speech and forwards it as text; the server sends it to the LLM and returns the reply as text; Sinch speaks it back."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"what-success-looks-like","__idx":11},"children":["What success looks like"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["The phone is answered and you hear the greeting (default ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Hello!"]},")."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["You speak; the bot answers in a natural voice."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["In the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["python server.py"]}," terminal you see the live transcript:",{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"header":{"controls":{"copy":{}}},"source":"[+] Connected: ('…', …)\n  WS << {\"command\":\"connect\", ...}\n  connect  callId=…  serviceId=…\n  WS >> {\"command\": \"answer\"}\n  WS >> {\"command\": \"text\", \"text\": \"Hello!\", ...}\n  STT → 'what can you do'\n  LLM ← 'I can answer questions over the phone…'\n  WS >> {\"command\": \"text\", \"text\": \"...\", \"isLast\": true, ...}\n"},"children":[]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["When you hang up, the session ends and history is discarded."]}]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"how-it-works","__idx":12},"children":["How it works"]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"the-voice-relay-protocol-text-only","__idx":13},"children":["The Voice Relay protocol (text only)"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Once Sinch dials the relay leg it opens a WebSocket and exchanges JSON messages. Your server never handles audio; STT and TTS happen on the Sinch side."]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Direction"},"children":["Direction"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"command"},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["command"]}]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Description"},"children":["Description"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Sinch → Server"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["connect"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Session start; carries ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callId"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["serviceId"]},", and any ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callHeaders"]}," from the SVAML."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Server → Sinch"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["answer"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Accept the relay session."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Server → Sinch"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["text"]}," (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["isLast: true"]},")"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Text for Sinch to read to the caller via TTS."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Sinch → Server"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["text"]}," / ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["prompt"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Transcribed speech from the caller."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Sinch → Server"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["interrupt"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Caller spoke during TTS playback (barge-in)."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Sinch → Server"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dtmf"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Keypad digits pressed by the caller."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Sinch → Server"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["textPlaybackStart"]}," / ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Stop"]}," / ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Cancel"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["TTS playback lifecycle events."]}]}]}]}]},{"$$mdtype":"Tag","name":"blockquote","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Contract caveat: verify before relying on it."]}," This on-the-wire protocol is ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["not part of the OpenAPI/SVAML REST contract"]},". The spec defines only the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["VOICE_RELAY"]}," destination shape (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["endpoint"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ttsVoice"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sttLanguage"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["enableInterruptions"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callHeaders"]},"). The message names and fields above (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["connect"]},"/",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["answer"]},"/",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["text"]},"/",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["prompt"]},"/",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["interrupt"]},"/",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dtmf"]},"/",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["textPlayback*"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["isLast"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["isInterruptible"]},") reflect the shipped ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["server.py"]}," and its comment referencing an ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["agent-relay.asyncapi.yml"]}," that is ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["not present in this repo"]},". Treat the exact framing as \"what the example assumes,\" and confirm it against the current Voice Relay product documentation before building on it. If a field name has drifted, the symptom is a silent or one-sided call."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"how-the-agent-handles-a-turn","__idx":14},"children":["How the agent handles a turn"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["On ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["connect"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["server.py"]}," sends ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["answer"]}," plus an optional greeting (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["GREETING"]},", default ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Hello!"]},"). On each ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["text"]}," / ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["prompt"]},":"]},{"$$mdtype":"Tag","name":"ol","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["The transcribed text is appended to an in-memory conversation history."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["The full history plus the system prompt from ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["system_prompt.md"]}," is sent to the LLM via LangChain (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["LLM.ainvoke"]},")."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["The reply is returned as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["{\"command\": \"text\", \"text\": \"…\", \"isLast\": true, \"isInterruptible\": true}"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["The reply is appended to history so context persists across turns."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["History is per-connection and in memory; it resets when the call ends. ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["interrupt"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dtmf"]},", and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["textPlayback*"]}," events are logged and otherwise ignored."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"robustness-in-the-example-and-its-limits","__idx":15},"children":["Robustness in the example (and its limits)"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Socket close"]}," is handled: ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["websockets.exceptions.ConnectionClosed"]}," is caught and logged with its code/reason."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["LLM errors"]}," are caught per-turn and logged, but the example does ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["not"]}," send a fallback message to the caller, so a provider failure leaves the caller in silence. For production, send a short apology ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["text"]}," message in the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["except"]}," branch."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["No reconnection logic"]}," is needed on the server side. Sinch opens the socket; if it drops, the session simply ends."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["The reply is sent as a single ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["text"]}," with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["isLast: true"]},". Streaming token-by-token (multiple ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["text"]}," messages with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["isLast: false"]}," then a final ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["isLast: true"]},") would lower perceived latency; the example keeps it simple with one full reply."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"customise-the-agent","__idx":16},"children":["Customise the agent"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Edit ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["system_prompt.md"]}," to change persona, domain, and behaviour. It is read once at startup, so restart ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["server.py"]}," after editing. The shipped persona is ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["RELAY"]},", a witty Sinch demo agent. To switch providers, change ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["PROVIDER"]}," + ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["API_KEY"]}," in ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":[".env"]}," and restart."]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"recommended-path-vs-fallback","__idx":17},"children":["Recommended path vs. fallback"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Headline provider:"]}," any of the three works. For phone conversations, ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["latency dominates the experience"]},", so pick a fast model (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["claude-haiku-4-5"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["gpt-4o-mini"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["gemini-2.0-flash"]},") over a slow flagship."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Voice Relay vs. Stream:"]}," use ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Voice Relay"]}," (this tutorial) when you want text in/out and let Sinch own STT/TTS. It is the simplest way to put an LLM on a call. Use ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Stream"]}," when you need raw bidirectional PCM audio."]}]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"production-readiness-checklist","__idx":18},"children":["Production-readiness checklist"]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Concern"},"children":["Concern"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"What to do"},"children":["What to do"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Latency budget"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Aim for sub-~1.5 s round-trip per turn. Use fast models; consider streaming the reply in chunks."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["LLM errors"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Add a fallback ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["text"]}," reply in the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["except"]}," branch so a provider failure doesn't leave the caller in silence."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Barge-in"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Leave ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["enableInterruptions: true"]}," (default) for natural turn-taking; set ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["false"]}," only when reading legal disclaimers etc. (an interrupt signal is still delivered so you can choose to stop playback)."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Conversation history"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The example keeps history in memory and loses it on disconnect. For production, persist by ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callId"]},"."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["PII / safety"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["LLM output goes straight to the caller. Add moderation/guardrails before forwarding TTS."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Custom metadata"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Use ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["voiceRelay.callHeaders[]"]}," (≤ 16 pairs) to pass tenant/campaign IDs into the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["connect"]}," event instead of URL params."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Static vs. webhook"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["STATIC suits \"every inbound call gets the same agent.\" For per-DID routing, use a webhook service that returns ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["commands"]}," computed per request."]}]}]}]}]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"voice-relay-config-reference","__idx":19},"children":["Voice Relay config reference"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["VOICE_RELAY"]}," is one of the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["to"]}," destination types, alongside ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["PHONE"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["SIP"]},", and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["STREAM"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["voiceRelay"]}," required fields: ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["endpoint"]}," (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["wss://"]}," URI, ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["format: uri"]},"), ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ttsVoice"]}," (string), ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sttLanguage"]}," (BCP-47, pattern ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["^[a-z]{2,3}(-[A-Z][a-z]{3})?(-([A-Z]{2}|[0-9]{3}))?$"]},")."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Optional: ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["enableInterruptions"]}," (boolean, default ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["true"]},"), ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callHeaders[]"]}," (array, ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["maxItems: 16"]},"; each item ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["{ key, value }"]},", each ≤ 255 chars)."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["The text-in/text-out WebSocket protocol between Sinch and your server is ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["not"]}," part of the REST/SVAML contract; see the contract caveat above."]}]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"files-in-this-tutorial","__idx":20},"children":["Files in this tutorial"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Find these files in ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"https://github.com/sinch/sinch-voice-tutorials"},"children":["repository"]},"."]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"File"},"children":["File"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Purpose"},"children":["Purpose"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["server.py"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["LangChain-backed Voice Relay WebSocket server (port 8765)."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["requirements.txt"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["websockets"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["python-dotenv"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["langchain-core"]},", and per-provider LangChain packages."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":[".env.example"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["PROVIDER"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["API_KEY"]},", optional ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["MODEL"]},"/",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["TEMPERATURE"]},"/",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["MAX_TOKENS"]},"/",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["PORT"]},". Copy to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":[".env"]},"."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["system_prompt.md"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The agent persona, loaded at startup."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["static.txt"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["A ready-made STATIC SVAML body (with a sample ngrok endpoint and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ttsVoice: Tiffany"]},") you can paste into the dashboard or the PATCH request."]}]}]}]}]}]},"headings":[{"value":"Connect an AI Chatbot (Voice Relay)","id":"connect-an-ai-chatbot-voice-relay","depth":1},{"value":"Overview","id":"overview","depth":2},{"value":"Real-life examples","id":"real-life-examples","depth":3},{"value":"Get the code","id":"get-the-code","depth":2},{"value":"Prerequisites","id":"prerequisites","depth":2},{"value":"Fastest path to first success","id":"fastest-path-to-first-success","depth":2},{"value":"1. Install dependencies","id":"1-install-dependencies","depth":3},{"value":"2. Set your LLM key","id":"2-set-your-llm-key","depth":3},{"value":"3. Start the server and expose it with ngrok","id":"3-start-the-server-and-expose-it-with-ngrok","depth":3},{"value":"4. Point a VOICE_RELAY destination at your server","id":"4-point-a-voice_relay-destination-at-your-server","depth":3},{"value":"5. Call the number and talk to the bot","id":"5-call-the-number-and-talk-to-the-bot","depth":3},{"value":"What success looks like","id":"what-success-looks-like","depth":3},{"value":"How it works","id":"how-it-works","depth":2},{"value":"The Voice Relay protocol (text only)","id":"the-voice-relay-protocol-text-only","depth":3},{"value":"How the agent handles a turn","id":"how-the-agent-handles-a-turn","depth":3},{"value":"Robustness in the example (and its limits)","id":"robustness-in-the-example-and-its-limits","depth":3},{"value":"Customise the agent","id":"customise-the-agent","depth":3},{"value":"Recommended path vs. fallback","id":"recommended-path-vs-fallback","depth":2},{"value":"Production-readiness checklist","id":"production-readiness-checklist","depth":2},{"value":"Voice Relay config reference","id":"voice-relay-config-reference","depth":2},{"value":"Files in this tutorial","id":"files-in-this-tutorial","depth":2}],"frontmatter":{"seo":{"title":"Connect an AI Chatbot (Voice Relay)"}},"lastModified":"2026-08-31T13:48:15.000Z","pagePropGetterError":{"message":"","name":""}},"slug":"/docs/voice-2.0/tutorials/voice-relay","userData":{"isAuthenticated":false,"teams":["anonymous"]},"isPublic":true}