{"templateId":"markdown","sharedDataIds":{"sidebar":"sidebar-docs/voice-2.0/sidebars.yaml"},"props":{"metadata":{"markdoc":{"tagList":[]},"product":"Voice v2 (preview)","type":"markdown"},"seo":{"title":"Build an AI IVR (Voice Relay + call patching)","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":"build-an-ai-ivr-voice-relay--call-patching","__idx":0},"children":["Build an AI IVR (Voice Relay + call patching)"]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"overview","__idx":1},"children":["Overview"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["This tutorial builds an ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["AI-powered IVR"]}," (Interactive Voice Response): a caller"," ","reaches an AI agent that ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["understands"]}," what they want, in their own words, and"," ","then ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["patches a human agent into the live call"]},". No DTMF menus (\"press 1 for"," ","sales\"), no fixed scripts."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["It combines two Voice API v2 capabilities:"]},{"$$mdtype":"Tag","name":"ol","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["(",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/docs/voice-2.0/tutorials/voice-relay"},"children":["Voice Relay"]},"): the call is"," ","bridged to a WebSocket server that exchanges ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["text"]}," with Sinch. Sinch does the"," ","speech-to-text and text-to-speech; your server only sees transcribed turns and"," ","replies with text. We use it here to run an LLM-driven intent classifier."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Live call patching"]}," is the new idea in this tutorial. While the call is"," ","running, your server sends a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["PATCH /v2/projects/{projectId}/calls/{callId}"]}," ","with fresh SVAML to ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["dial a human agent and bridge them into the existing"," ","call"]},". The AI then drops out and the caller is talking to a person."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The worked example is a house-selling company's call centre. The AI greets the"," ","caller, classifies the intent as ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Sales"]}," or ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Support"]},", tells the caller it's"," ","connecting them, and patches the matching agent into the call."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"call-flow","__idx":2},"children":["Call flow"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"header":{"controls":{"copy":{}}},"source":"   ┌──────────┐   incoming call    ┌─────────────────────────────────────────┐\n   │  Caller  │ ─────────────────► │  Sinch Voice API v2                      │\n   └──────────┘                    │                                          │\n                                   │  answer → bridge(\"ivr-bridge\")           │\n                                   │        → dial VOICE_RELAY → bridge        │\n                                   └───────────────┬──────────────────────────┘\n                                                   │  wss:// (text protocol)\n                                                   ▼\n                                   ┌──────────────────────────────────────────┐\n                                   │  Your relay server (this tutorial)        │\n                                   │  1. greet the caller                      │\n                                   │  2. classify each turn with an LLM        │\n                                   │  3. once classified → PATCH the call:     │\n                                   │       dial Sales/Support + bridge in      │\n                                   │  4. close the socket (AI leaves)          │\n                                   └───────────────┬──────────────────────────┘\n                                                   │  PATCH /calls/{callId}\n                                                   ▼\n              ┌──────────┐  bridged   ┌─────────────────────────┐\n              │  Caller  │ ◄────────► │  Sales / Support agent   │\n              └──────────┘            └─────────────────────────┘\n"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The bridge named ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ivr-bridge"]}," is the key. The caller leg and the relay leg are"," ","both bridged into it on the way in. The PATCH dials the agent and bridges them"," ","into ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["the same"]}," named bridge, so when the relay socket closes, the caller and"," ","the agent remain connected."]},{"$$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 the scripts 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.5-ai-ivr/"]},". Clone it and move into the tutorial folder:"]},{"$$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/4.5-ai-ivr\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"repository-layout","__idx":4},"children":["Repository layout"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["This tutorial is ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["self-contained"]},": its ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":[".env"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":[".env.example"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["system_prompt.md"]},","," ","and dependency manifests all live in the tutorial folder, and every script reads"," ","them from there. Two paths are resolved by convention (both relative to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["scripts/"]},"):"," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["system_prompt.md"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":[".env"]}," sit one level up, at the tutorial folder root."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"header":{"controls":{"copy":{}}},"source":"4.5-ai-ivr/                     this tutorial (self-contained)\n├── description.md              this file\n├── .env.example                template listing every variable\n├── .env                        you create this (cp .env.example .env)\n├── system_prompt.md            the intent-classifier prompt\n├── requirements.txt            Python deps (websockets, requests)\n├── package.json                Node dep (ws)\n├── composer.json               PHP dep (cboden/ratchet)\n└── scripts/\n    ├── relay-server.py         relay server (Python)\n    ├── relay-server.node.js    relay server (Node.js)\n    ├── relay-server.php        relay server (PHP)\n    ├── configure-service.sh    set the STATIC inbound behavior\n    ├── test-callout.sh         simulate an inbound call\n    └── patch-call.sh           patch a live call by hand\n"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Run the shell commands below from the tutorial folder (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["4.5-ai-ivr/"]},") unless a"," ","step says otherwise."]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"quick-start-minimal-end-to-end-first-success","__idx":5},"children":["Quick start (minimal end-to-end first success)"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["This is the shortest path from nothing to \"I called, said ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["sales"]},", and a phone"," ","rang.\" Reference material (PATCH mechanics, prompt tuning, provider switching)"," ","follows further down. There is a lot of setup here (service config, a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["wss://"]}," ","tunnel, a relay server, and an LLM key), so do the steps ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["in order"]},"; each one"," ","depends on the previous."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"# 0. From the tutorial folder (after cloning; see \"Get the code\")\ncd sinch-voice-tutorials/4.5-ai-ivr\ncp .env.example .env\n\n# 1. Fill in .env in this folder (see the Setup table below) — at minimum:\n#    PROJECT_ID, KEY_ID, KEY_SECRET, SERVICE_ID, SINCH_NUMBER,\n#    SALES_NUMBER, SUPPORT_NUMBER, LLM_API_KEY (+ LLM_BASE_URL/LLM_MODEL)\n\n# 2. Install one runtime's dependency\npip install -r requirements.txt        # Python:  websockets, requests\n# or  npm install                       # Node:    ws\n# or  composer install                  # PHP:     cboden/ratchet\n\n# 3. Start the relay server (verifies your config locally before any call routes)\npython scripts/relay-server.py          # logs the model + listening port\n# or  node scripts/relay-server.node.js\n# or  php scripts/relay-server.php\n\n# 4. In a second terminal, expose it and capture the wss:// URL\nngrok http 8765\n#    -> set WS_ENDPOINT=wss://<id>.ngrok-free.app in .env\n\n# 5. Point the service's STATIC behavior at that URL\nbash scripts/configure-service.sh       # reads WS_ENDPOINT from .env\n#    (or: bash scripts/configure-service.sh wss://<id>.ngrok-free.app)\n\n# 6. Trigger a call — dial your Sinch number, OR simulate it:\nbash scripts/test-callout.sh            # calls DESTINATION_NUMBER, bridges relay\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"blockquote","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Why start the server (step 3) before configuring the service (step 5)?"]}," ","Starting the relay server is the only ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["local"]}," check you have: it loads ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":[".env"]},","," ","exits immediately with a clear ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ERROR: <VAR> is not set"]}," if anything is"," ","missing, and prints the LLM model it will use. Catch credential and dependency"," ","problems here, on your machine, before a real call is silently failing on the"," ","wire. The default port is ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["8765"]},"; the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ngrok"]}," command and the server must agree"," ","on it (set ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["PORT"]}," in ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":[".env"]}," to change both; see the note in Step 4 of the"," ","detailed walkthrough)."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"what-success-looks-like","__idx":6},"children":["What success looks like"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["You dial the Sinch number (or answer the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["test-callout.sh"]}," call). You hear:"]},{"$$mdtype":"Tag","name":"blockquote","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["\"Hello, this is the call centre. How can I help you?\""]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["You say ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["\"I'd like to buy a house\""]},". After a short pause you hear:"]},{"$$mdtype":"Tag","name":"blockquote","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["\"Please wait, connecting you to Sales.\""]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["…and the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["SALES_NUMBER"]}," phone rings. When that agent answers they hear"," ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["\"Connecting you to a customer. Intent: Sales.\""]}," and are bridged to you; the AI"," ","has dropped off the line. The relay server log shows:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"header":{"controls":{"copy":{}}},"source":"[+] connected\n  << {\"command\":\"connect\",\"callId\":\"01ARZ...\"}\n  >> {\"command\":\"answer\"}\n  >> {\"command\":\"text\",\"text\":\"Hello, this is the call centre. How can I help you?\",\"isLast\":true}\n  << {\"command\":\"text\",\"text\":\"I'd like to buy a house\"}\n  >> {\"command\":\"text\",\"text\":\"Please wait, connecting you to Sales.\",\"isLast\":true}\n[*] patched Sales into call 01ARZ...\n[-] session ended  callId=01ARZ...\n"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Saying ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["\"my heating is broken\""]}," routes to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["SUPPORT_NUMBER"]}," instead. Saying"," ","something ambiguous (",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["\"I have a question\""]},") gets a spoken follow-up question"," ","rather than a transfer."]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"setup","__idx":7},"children":["Setup"]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"prerequisites","__idx":8},"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,"," ","a virtual number, and a Voice ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["service"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["A publicly reachable WebSocket endpoint. Use ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"https://ngrok.com"},"children":["ngrok"]}," in dev:",{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"ngrok http 8765    # exposes localhost:8765 as wss://<id>.ngrok-free.app\n","lang":"bash"},"children":[]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["One runtime for the relay server: ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Python 3.10+"]}," (with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["pip"]},"), ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Node.js 18+"]}," ","(global ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["fetch"]}," is used, so 18+ is required), or ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["PHP 8.1+"]}," (with the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["curl"]}," ","extension)."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["An LLM API key. The classifier talks to any ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["OpenAI-compatible"]}," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["/chat/completions"]}," endpoint, so it is model-agnostic; point it at OpenAI,"," ","at Anthropic's compatibility endpoint for Claude, or at a local gateway."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"environment-variables","__idx":9},"children":["Environment variables"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The relay server, ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["configure-service.sh"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["patch-call.sh"]},", and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["test-callout.sh"]}," ","all read this tutorial's own ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":[".env"]},", which lives at the ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["tutorial folder root"]}," ","(",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["../.env"]}," relative to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["scripts/"]},"). Copy the example and fill in the values:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"cp .env.example .env\n","lang":"bash"},"children":[]},{"$$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":"Variable"},"children":["Variable"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Used for"},"children":["Used for"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Where to get it"},"children":["Where to get it"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["PROJECT_ID"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["KEY_ID"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["KEY_SECRET"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Sinch auth (HTTP Basic) for the service PATCH and the live-call PATCH."]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"https://dashboard.sinch.com"},"children":["Sinch dashboard"]}," → Access keys."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["SERVICE_ID"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The service whose call behavior you configure (Step 1)."]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["GET /v2/projects/{projectId}/services"]}," or the dashboard."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["SINCH_NUMBER"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Caller-ID (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["from"]},") used when dialing the agent and (for testing) the customer."]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Your provisioned virtual number, E.164."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["WS_ENDPOINT"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Public ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["wss://"]}," URL of your relay server (goes in the SVAML)."]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ngrok"]}," forwarding URL (Step 3/4)."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["SALES_NUMBER"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["SUPPORT_NUMBER"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The two human queues the call can be patched into."]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Any reachable phones, E.164."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["LLM_BASE_URL"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["LLM_API_KEY"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["LLM_MODEL"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The OpenAI-compatible model that classifies intent."]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["See the provider note below."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["DESTINATION_NUMBER"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Only for ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["test-callout.sh"]},": your own phone, to simulate an inbound call."]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Your phone, E.164."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["PORT"]}," ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["(optional)"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Port the relay server listens on (default ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["8765"]},")."]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Must match the port you pass to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ngrok http"]},"."]}]}]}]}]},{"$$mdtype":"Tag","name":"blockquote","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Heads-up."]}," This tutorial ships its own ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":[".env.example"]}," (listing every"," ","variable above), so a fresh ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["cp .env.example .env"]}," has all the keys. The relay"," ","server still fails fast with a clear message if a required key is missing;"," ","separately, make sure ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["WS_ENDPOINT"]}," is ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["wss://"]}]}," (Sinch requires TLS for"," ","Voice Relay), not a plain ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ws://"]}," URL."]}]},{"$$mdtype":"Tag","name":"blockquote","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Where do ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sessionId"]}," / ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callName"]}," come from for ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["patch-call.sh"]},"?"]}," ","You don't need them for the automatic flow: the relay server patches by"," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callId"]},", which it reads from the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["connect"]}," frame. ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["patch-call.sh"]}," also takes a"," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callId"]}," (its only positional argument besides the intent). If you want to use"," ","the session-keyed PATCH endpoint instead, both ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sessionId"]}," and the leg's"," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callName"]}," are available: ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sessionId"]}," is returned by the outbound call"," ","(",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["test-callout.sh"]}," prints the full call object) and appears in the relay"," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["connect"]}," metadata, and the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callName"]}," of the inbound/caller leg is ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["caller"]}," ","(set in the STATIC behavior). See ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["Patching by session + call name"]}," below."]}]},{"$$mdtype":"Tag","name":"blockquote","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Choosing and configuring the LLM (provider switch)."]}," ","The classifier is model-agnostic; only three variables change between providers."," ","A fast, cheap model is the right call here: classification is a one-word answer"," ","and every round-trip is silence the caller hears."]},{"$$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":["Provider"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"LLM_BASE_URL"},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["LLM_BASE_URL"]}]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"LLM_MODEL"},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["LLM_MODEL"]}]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"LLM_API_KEY"},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["LLM_API_KEY"]}]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["OpenAI"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["https://api.openai.com/v1"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["gpt-4o-mini"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["from platform.openai.com"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Claude (Anthropic)"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["https://api.anthropic.com/v1"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["claude-haiku-4-5-20251001"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["from console.anthropic.com"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Local gateway"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["http://localhost:<port>/v1"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["gateway-specific"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["gateway-specific"]}]}]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Anthropic exposes an OpenAI-compatible ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["/chat/completions"]}," route, so pointing"," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["LLM_BASE_URL"]}," at it needs ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["no code changes"]},". Defaults if unset:"," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["LLM_BASE_URL=https://api.openai.com/v1"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["LLM_MODEL=gpt-4o-mini"]}," ","(",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["LLM_API_KEY"]}," is always required)."]}]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"step-by-step-instructions","__idx":10},"children":["Step-by-step instructions"]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"1-configure-the-inbound-call-behavior","__idx":11},"children":["1. Configure the inbound call behavior"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["When a call comes in, Sinch must answer it, bridge it, and dial the Voice"," ","Relay leg. Because that SVAML is the same for every inbound call, the simplest"," ","setup is a ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["STATIC"]}," call behavior on the service. No webhook server is needed"," ","for call setup (the relay WebSocket server is the only thing you run)."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"bash scripts/configure-service.sh\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["This PATCHes your service (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["PATCH /v2/projects/{projectId}/services/{serviceId}"]},")"," ","with the SVAML below (filled in from ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":[".env"]},"):"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"jsonc","header":{"controls":{"copy":{}}},"source":"{\n  \"callBehavior\": {\n    \"type\": \"STATIC\",\n    \"static\": {\n      \"callName\": \"caller\",\n      \"commands\": [\n        { \"command\": \"answer\" },\n        { \"command\": \"bridgeCall\", \"bridgeName\": \"ivr-bridge\" },\n        {\n          \"command\": \"dial\",\n          \"callName\": \"voice_relay_call\",\n          \"to\": {\n            \"type\": \"VOICE_RELAY\",\n            \"voiceRelay\": {\n              \"endpoint\":    \"wss://<your-ngrok-id>.ngrok-free.app\",\n              \"ttsVoice\":    \"Tiffany\",\n              \"sttLanguage\": \"en-US\"\n            }\n          },\n          \"events\": {\n            \"onAnswer\": [ { \"command\": \"bridgeCall\", \"bridgeName\": \"ivr-bridge\" } ]\n          }\n        }\n      ],\n      \"events\": {\n        \"onHangup\": [ { \"command\": \"hangup\", \"callName\": \"voice_relay_call\" } ]\n      }\n    }\n  }\n}\n","lang":"jsonc"},"children":[]},{"$$mdtype":"Tag","name":"blockquote","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Why ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callName: \"caller\""]},"."]}," Naming the inbound leg lets the PATCH later hang"," ","it up cleanly (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["{\"command\":\"hangup\",\"callName\":\"caller\"}"]}," when the agent ends"," ","the call). The relay leg is named ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["voice_relay_call"]}," so the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onHangup"]}," can tear"," ","it down if the caller drops first."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"2-start-the-relay-server","__idx":12},"children":["2. Start the relay server"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Pick one implementation. Each listens on ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["PORT"]}," (default ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["8765"]},") and speaks the"," ","Voice Relay text protocol. Run from the ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["tutorial folder root"]},": the relay"," ","servers locate ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["system_prompt.md"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":[".env"]}," at the tutorial folder root"," ","(",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["../system_prompt.md"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["../.env"]}," relative to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["scripts/"]},")."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"# Python — pip install -r requirements.txt\npython scripts/relay-server.py\n\n# Node.js — npm install (installs ws)\nnode scripts/relay-server.node.js\n\n# PHP — composer install (installs cboden/ratchet)\nphp scripts/relay-server.php\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["On startup it logs the LLM model in use and the listening port, e.g."," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["[*] AI IVR relay  model=gpt-4o-mini  listening on ws://0.0.0.0:8765"]},". If a"," ","required variable is missing it exits immediately with"," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ERROR: <NAME> is not set in the environment / .env"]},"."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"3-expose-it-and-point-the-svaml-at-it","__idx":13},"children":["3. Expose it and point the SVAML at it"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"ngrok http 8765      # use the same number as PORT\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Copy the forwarding URL, set ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["WS_ENDPOINT=wss://<id>.ngrok-free.app"]}," in ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":[".env"]},","," ","and re-run ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["bash scripts/configure-service.sh"]}," so the STATIC SVAML uses the new"," ","URL. (You can also pass it as an argument:"," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["bash scripts/configure-service.sh wss://<id>.ngrok-free.app"]},".)"]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"4-call-your-sinch-number--or-simulate-it","__idx":14},"children":["4. Call your Sinch number — or simulate it"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Dial your Sinch number from any phone. You should hear the greeting, and after"," ","you state your intent (\"I'd like to buy a house\" / \"my heating is broken\") the AI"," ","says it's connecting you and the agent's phone rings."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["No spare inbound number? Simulate the whole flow with an outbound self-callout"," ","that dials ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["you"]}," as the \"caller\" and bridges in the relay leg exactly as the"," ","inbound behavior would (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["POST /v2/projects/{projectId}/calls"]},"):"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"bash scripts/test-callout.sh        # dials DESTINATION_NUMBER, bridges in the relay\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["This prints the created call object (including ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sessionId"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callId"]},") on"," ","HTTP 201."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"5-optional-patch-a-call-by-hand","__idx":15},"children":["5. (Optional) Patch a call by hand"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["To understand the PATCH in isolation, separate from the LLM, grab a live"," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callId"]}," (from the relay server logs, or ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["GET /v2/projects/{projectId}/calls"]},")"," ","and run:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"bash scripts/patch-call.sh <callId> sales      # or: support\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["This is exactly the request the relay server sends automatically once it has"," ","classified the intent. It expects HTTP ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["202 Accepted"]}," and exits non-zero on"," ","anything else."]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"how-it-works","__idx":16},"children":["How it works"]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"the-voice-relay-text-protocol","__idx":17},"children":["The Voice Relay text protocol"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Once Sinch dials the relay leg it opens a WebSocket to your server and exchanges"," ","JSON text frames (raw audio never reaches you):"]},{"$$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":["Command"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Meaning"},"children":["Meaning"]}]}]},{"$$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"]}," (the relay leg) and metadata."]}]},{"$$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 speak (TTS) to 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":["text"]}," / ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["prompt"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The caller's transcribed speech (STT)."]}]},{"$$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":"code","attributes":{},"children":["dtmf"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["textPlayback*"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Barge-in, keypad, and playback lifecycle (logged, not needed here)."]}]}]}]}]},{"$$mdtype":"Tag","name":"blockquote","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The Voice Relay on-the-wire text protocol is ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["not"]}," part of the OpenAPI spec;"," ","it is documented separately as part of the Voice Relay product surface. The"," ","field names above (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["connect"]},"/",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callId"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["text"]},"/",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["prompt"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["isLast"]},") are what the"," ","example servers send and expect; verify them against the current Voice Relay"," ","protocol docs before going to production."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"the-classify-then-patch-loop","__idx":18},"children":["The classify-then-patch loop"]},{"$$mdtype":"Tag","name":"ol","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["On ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["connect"]},", capture ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callId"]},", send ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["answer"]},", then greet the caller with a"," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["text"]}," frame."]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["On each ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["text"]},"/",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["prompt"]},", send the caller's words to the LLM with the"," ","classification prompt from ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["system_prompt.md"]},"."]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The model replies with a single word (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Sales"]},"/",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Support"]},") when it is confident,"," ","or a short sentence asking the caller to clarify. The server uses a simple"," ","rule: ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["a one-word reply that matches a known intent is a routing decision;"," ","anything else is spoken back"]}," to the caller as a follow-up question."]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["On a routing decision, the server speaks \"Please wait, connecting you to"," ","Sales/Support\", waits briefly (~2.5 s) so the TTS is heard, then PATCHes the"," ","call:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"jsonc","header":{"controls":{"copy":{}}},"source":"PATCH /v2/projects/{projectId}/calls/{callId}\n{\n  \"commands\": [\n    {\n      \"command\": \"dial\",\n      \"callName\": \"agent_call\",\n      \"from\": { \"type\": \"PHONE\", \"phone\": { \"number\": \"<SINCH_NUMBER>\" } },\n      \"to\":   { \"type\": \"PHONE\", \"phone\": { \"number\": \"<SALES_or_SUPPORT>\" } },\n      \"dialTimeoutDurationSeconds\": 20,\n      \"maxCallDurationSeconds\": 3600,\n      \"events\": {\n        \"onAnswer\": [\n          { \"command\": \"bridgeCall\", \"bridgeName\": \"ivr-bridge\" },\n          { \"command\": \"messages\", \"messagesName\": \"agent-intro\",\n            \"messages\": [ { \"type\": \"SAY\",\n              \"say\": { \"format\": \"TEXT\",\n                       \"text\": \"Connecting you to a customer. Intent: Sales.\",\n                       \"voiceName\": \"Tiffany\" } } ] }\n        ],\n        \"onHangup\": [ { \"command\": \"hangup\", \"callName\": \"caller\" } ]\n      }\n    }\n  ]\n}\n","lang":"jsonc"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The request body is a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callPatchRequest"]}," (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["{ \"commands\": [...] }"]},") and the API"," ","returns ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["202 Accepted"]},". The ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dial"]}," command's fields (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callName"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["from"]},","," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["to"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dialTimeoutDurationSeconds"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["maxCallDurationSeconds"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["events"]},"), the"," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["bridgeCall.bridgeName"]},", and the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["messages"]},"/",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["SAY"]}," shape all match the spec"," ","exactly. An optional ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Idempotency-Key"]}," header is sent (see below)."]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The server closes the WebSocket. The relay leg leaves ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ivr-bridge"]},"; the caller"," ","and the freshly-dialed agent stay bridged."]}]}]},{"$$mdtype":"Tag","name":"blockquote","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Which ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callId"]}," do you patch?"]}," The ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callId"]}," from the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["connect"]}," frame is the"," ","relay leg. Patching it adds the new ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dial"]}," to the ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["same session"]},", and"," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["bridgeCall"]}," with the existing ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["bridgeName"]}," drops the agent into the bridge the"," ","caller is already in."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"patching-by-session--call-name","__idx":19},"children":["Patching by session + call name"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The spec offers an equivalent endpoint keyed by session and call name:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"header":{"controls":{"copy":{}}},"source":"PATCH /v2/projects/{projectId}/sessions/{sessionId}/calls/{callName}\n"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["It takes the same ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callPatchRequest"]}," body and returns 202. Use it when you have"," ","the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sessionId"]}," (from the call object or the relay metadata) and a stable leg"," ","name rather than a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callId"]},"; for example, to target the inbound ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["caller"]}," leg"," ","by name. The example servers and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["patch-call.sh"]}," use the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callId"]}," form because"," ","the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callId"]}," arrives directly in the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["connect"]}," frame."]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"production-readiness-checklist","__idx":20},"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":["Every classifier round-trip is silence on the line. Use a fast, cheap model and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["temperature: 0"]}," (the examples set both). Keep the system prompt short."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["One patch per call"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The examples guard with a per-connection ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["patched"]}," flag so a second transcribed turn can't dial a second agent. (The PHP server sets the flag ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["before"]}," the timer fires, closing a small race the others leave open by closing the socket synchronously.)"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Classifier errors / timeouts"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The examples wrap the LLM call in try/catch with a 10 s timeout; on failure they speak \"Sorry, please try again.\" and keep the line open rather than crashing or hanging up."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Ambiguous intent"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["A reply that isn't a known one-word route is spoken back verbatim as a clarifying question, so the caller can refine, with no silent mis-route."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Agent doesn't answer"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The example dials with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dialTimeoutDurationSeconds: 20"]},". Add an ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onTimeout"]}," branch on the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dial"]}," to fall back to voicemail or another queue (see ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/docs/voice-2.0/tutorials/call-forwarding"},"children":["Call Forwarding"]},"). The example does ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["not"]}," yet handle this; add it before production."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Idempotency"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The PATCH endpoint accepts an ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Idempotency-Key"]}," header; the examples send ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["${callId}-${intent}"]}," so a retried PATCH can't create two agent legs."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Classifier guardrails"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["LLM output is parsed with a strict \"one word that matches a known intent = route\" rule and an allow-list (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["{sales, support}"]},"). Never feed model text straight into a phone-number lookup."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Secrets"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The relay server holds your Sinch ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["and"]}," LLM credentials. Load them from the environment (the examples read ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["../.env"]},"); never hard-code keys."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Per-DID routing"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["STATIC applies the same SVAML to every inbound call. For different agents per inbound number, use a WEBHOOK service that returns the same ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["commands"]}," (plus ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callName"]},"/",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["events"]},") per request (see ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/docs/voice-2.0/tutorials/inbound-pstn"},"children":["Inbound PSTN"]},")."]}]}]}]}]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"real-life-examples","__idx":21},"children":["Real-life examples"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Smart call routing"]},": replace a multi-level DTMF phone tree with one AI that"," ","routes on intent (\"I want to book a viewing\" → Sales)."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Tier-0 triage"]},": the AI handles FAQs and only patches in a human when it"," ","decides the caller needs one."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Overflow / after-hours"]},": the AI qualifies the caller, then patches in"," ","whichever queue is staffed."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Warm transfer with context"]},": the PATCH plays an intro message to the agent"," ","(\"Connecting a customer interested in buying\") before bridging the caller."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"what-the-openapi-spec-says-at-a-glance","__idx":22},"children":["What the OpenAPI spec says at a glance"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["PATCH /v2/projects/{projectId}/calls/{callId}"]}," takes a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callPatchRequest"]}," ","(",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["{ \"commands\": [...] }"]},") and returns ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["202 Accepted"]},". It accepts an optional"," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Idempotency-Key"]}," header. ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["(Verified.)"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["PATCH /v2/projects/{projectId}/sessions/{sessionId}/calls/{callName}"]}," is the"," ","equivalent keyed by session + call name; same body, same 202. ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["(Verified.)"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["An inbound call is configured by setting the service's ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callBehavior"]},". A"," ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["STATIC"]}," behavior is ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["{ \"type\": \"STATIC\", \"static\": { \"commands\": [...], \"callName\"?, \"events\"?: { \"onHangup\": [...] } } }"]},"; there is no ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["accept"]}," ","command. ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["(Verified.)"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["voiceRelay"]}," (a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["to"]}," discriminator value, ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["type: VOICE_RELAY"]},") requires"," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["endpoint"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ttsVoice"]},", and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sttLanguage"]},"; ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["enableInterruptions"]}," defaults to"," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["true"]},". ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["(Verified.)"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dial"]}," requires ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["command"]}," + ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["to"]},"; ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callName"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["from"]},","," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dialTimeoutDurationSeconds"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["maxCallDurationSeconds"]},", and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["events"]}," ","(",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onAnswer"]},"/",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onTimeout"]},"/",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onHangup"]},") are all valid. ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["(Verified.)"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["bridgeCall"]}," adds the current leg to a named bridge; bridging multiple legs"," ","into the same ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["bridgeName"]}," is what connects the parties. ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["(Verified.)"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["messages"]}," carries a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["messagesName"]}," and a list of ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["SAY"]},"/",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["PLAY"]}," items; ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["SAY"]}," ","requires ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["say.text"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["say.voiceName"]},", with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["say.format"]}," of ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["TEXT"]}," (default)"," ","or ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["SSML"]},". ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["(Verified.)"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["The Voice Relay on-the-wire text protocol is ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["not"]}," part of the OpenAPI spec;"," ","it is documented separately as part of the Voice Relay product surface."]}]}]},"headings":[{"value":"Build an AI IVR (Voice Relay + call patching)","id":"build-an-ai-ivr-voice-relay--call-patching","depth":1},{"value":"Overview","id":"overview","depth":2},{"value":"Call flow","id":"call-flow","depth":3},{"value":"Get the code","id":"get-the-code","depth":2},{"value":"Repository layout","id":"repository-layout","depth":3},{"value":"Quick start (minimal end-to-end first success)","id":"quick-start-minimal-end-to-end-first-success","depth":2},{"value":"What success looks like","id":"what-success-looks-like","depth":3},{"value":"Setup","id":"setup","depth":2},{"value":"Prerequisites","id":"prerequisites","depth":3},{"value":"Environment variables","id":"environment-variables","depth":3},{"value":"Step-by-step instructions","id":"step-by-step-instructions","depth":2},{"value":"1. Configure the inbound call behavior","id":"1-configure-the-inbound-call-behavior","depth":3},{"value":"2. Start the relay server","id":"2-start-the-relay-server","depth":3},{"value":"3. Expose it and point the SVAML at it","id":"3-expose-it-and-point-the-svaml-at-it","depth":3},{"value":"4. Call your Sinch number — or simulate it","id":"4-call-your-sinch-number--or-simulate-it","depth":3},{"value":"5. (Optional) Patch a call by hand","id":"5-optional-patch-a-call-by-hand","depth":3},{"value":"How it works","id":"how-it-works","depth":2},{"value":"The Voice Relay text protocol","id":"the-voice-relay-text-protocol","depth":3},{"value":"The classify-then-patch loop","id":"the-classify-then-patch-loop","depth":3},{"value":"Patching by session + call name","id":"patching-by-session--call-name","depth":3},{"value":"Production-readiness checklist","id":"production-readiness-checklist","depth":2},{"value":"Real-life examples","id":"real-life-examples","depth":2},{"value":"What the OpenAPI spec says at a glance","id":"what-the-openapi-spec-says-at-a-glance","depth":2}],"frontmatter":{"seo":{"title":"Build an AI IVR (Voice Relay + call patching)"}},"lastModified":"2026-08-31T13:48:15.000Z","pagePropGetterError":{"message":"","name":""}},"slug":"/docs/voice-2.0/tutorials/ai-ivr","userData":{"isAuthenticated":false,"teams":["anonymous"]},"isPublic":true}