{"templateId":"markdown","sharedDataIds":{"sidebar":"sidebar-docs/voice-2.0/sidebars.yaml"},"props":{"metadata":{"markdoc":{"tagList":[]},"product":"Voice v2 (preview)","type":"markdown"},"seo":{"title":"Stream Call Audio in Real-Time","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":"stream-call-audio-in-real-time","__idx":0},"children":["Stream Call Audio in Real-Time"]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"overview","__idx":1},"children":["Overview"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The Sinch Voice API v2 can stream live PCM audio from a phone call bidirectionally to a WebSocket server. This unlocks feeding raw audio into AI models, speech-to-text engines, sentiment analysis pipelines, or custom voice bots, and it lets you send synthesized audio back to the caller over the same socket."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["When a call leg is routed to a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["STREAM"]}," destination, Sinch opens a WebSocket connection to your ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["endpoint"]}," and exchanges audio as binary frames. To get a working echo loop you only need three things running, in this order:"]},{"$$mdtype":"Tag","name":"ol","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["A WebSocket echo server (reads binary PCM, writes it straight back)."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["ngrok exposing that server as a public ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["wss://"]}," URL."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["An API call whose SVAML dials a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["STREAM"]}," leg pointed at that URL, bridged with a phone leg."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The rest of this document is reference: the PCM/",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["streamOptions"]}," contract, the inbound (ICE webhook) path, the on-the-wire message protocol, and the other language servers."]},{"$$mdtype":"Tag","name":"blockquote","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["When to use STREAM vs. Voice Relay."]}," Use ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["STREAM"]}," when you want the ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["raw audio"]},": you run your own STT/TTS, or you forward the PCM to a third-party agent. If you'd rather have Sinch do the speech-to-text and text-to-speech for you and just exchange ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["text"]}," turns, use ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["VOICE_RELAY"]}," instead. See ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/docs/voice-2.0/tutorials/voice-relay"},"children":["Connect an AI Chatbot (Voice Relay)"]},". If you want a fully external agent SDK (e.g. ElevenLabs Conversational AI) on the far end of the stream, see ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/docs/voice-2.0/tutorials/elevenlabs"},"children":["ElevenLabs Bridge"]},". For a deeper walkthrough of the server side itself, see ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/docs/voice-2.0/tutorials/websocket-agent"},"children":["WebSocket Server"]},"."]}]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"setup-do-this-once","__idx":2},"children":["Setup (do this once)"]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"get-the-code","__idx":3},"children":["Get the code"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["All 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. Clone it and move into this tutorial's 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.2-stream-audio\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The layout you'll be working with:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"header":{"controls":{"copy":{}}},"source":"sinch-voice-tutorials/\n├── .env                       # shared credentials (repository / tutorials root)\n├── 4.2-stream-audio/\n│   ├── description.md          # this document\n│   └── scripts/\n│       ├── ws-server.py\n│       ├── ws-server.node.js\n│       ├── ws-server.php\n│       ├── WsServer.java\n│       ├── ice-callback.py\n│       ├── ice-callback.node.js\n│       ├── trigger-call.sh\n│       └── trigger-call.js\n└── ...                         # sibling tutorials (4.1, 4.3, 4.4)\n"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Every command below assumes your working directory is ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sinch-voice-tutorials/4.2-stream-audio"]},", so the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["scripts/..."]}," paths resolve. The scripts read the shared ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":[".env"]}," two levels up at the repository root (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["../../.env"]},")."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"credentials-and-env","__idx":4},"children":["Credentials and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":[".env"]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["All scripts read the shared ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":[".env"]}," at the repository root (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sinch-voice-tutorials/.env"]},"). Auth is HTTP Basic (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["KEY_ID:KEY_SECRET"]},"). Create the file at the repo root (copy ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":[".env.example"]}," if the repo ships one) and fill in:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"PROJECT_ID=...            # Sinch project\nKEY_ID=...                # API key id  (Basic auth username)\nKEY_SECRET=...            # API key secret (Basic auth password)\nSINCH_NUMBER=+1...        # your Sinch virtual number (E.164)\nDESTINATION_NUMBER=+1...  # the phone that will ring (E.164)\nWS_ENDPOINT=wss://...     # public WebSocket URL from ngrok (see below); must be wss:// or ws://\nPORT=...                  # port for the ICE webhook server (inbound path only; optional)\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["WS_ENDPOINT"]}," ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["must"]}," be a WebSocket URL (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["wss://"]}," recommended, ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ws://"]}," allowed) and reachable from the public internet. It is ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["not"]}," an ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["https://"]}," URL. Convert your ngrok forwarding URL (see step 2 below)."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"what-about-ice-callback-","__idx":5},"children":["What about ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ice-callback"]},"?"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ice-callback.{node.js,py}"]}," is ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["only needed for the inbound path"]},": when a caller dials ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["your"]}," Sinch number and you want Sinch to route that call into your WebSocket server. It is an HTTP webhook server that handles the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.incoming"]}," event and replies with SVAML that bridges the inbound PSTN leg to a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["STREAM"]}," leg."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["For the first-success path below you do NOT need ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ice-callback"]},"."]}," The outbound trigger (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["trigger-call.{sh,js}"]},") embeds the SVAML inline and calls the API directly. Skip ahead to \"Inbound calls\" only when you want callers to reach your stream."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"prerequisites","__idx":6},"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 API credentials and a Sinch virtual number."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["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 cloned (see \"Get the code\" above)."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"https://ngrok.com"},"children":["ngrok"]}," (or any public tunnel) for development."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["One server runtime: Node.js 18+ (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ws"]},"), Python 3.8+ (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["websockets"]},"), PHP 8+ (Ratchet), or Java 11+ (Tyrus)."]}]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"first-success-hear-your-own-voice-echoed-back","__idx":7},"children":["First success: hear your own voice echoed back"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["This is the shortest loop that proves the stream works end to end. The Python server below echoes every binary PCM frame straight back, so when the call connects you should hear yourself."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"1-start-the-echo-websocket-server","__idx":8},"children":["1. Start the echo WebSocket server"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"pip install websockets\npython scripts/ws-server.py\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["It listens on port ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["8765"]}," (override with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["WS_PORT"]},"). The Python server in this tutorial already echoes audio back (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["await websocket.send(message)"]},"), so it doubles as an echo demo. The Node, PHP, and Java servers in ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["scripts/"]}," log/record audio but do ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["not"]}," echo by default. See \"Sending audio back\" for the one-line change."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"2-expose-it-with-ngrok","__idx":9},"children":["2. Expose it with ngrok"]},{"$$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 Forwarding URL and convert the scheme to a WebSocket URL:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["https://abc123.ngrok-free.app"]}," becomes ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["wss://abc123.ngrok-free.app"]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Set it in ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":[".env"]}," (or export it):"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"export WS_ENDPOINT=wss://abc123.ngrok-free.app\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"3-trigger-the-call","__idx":10},"children":["3. Trigger the call"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"bash scripts/trigger-call.sh\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["This dials ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["DESTINATION_NUMBER"]}," from ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["SINCH_NUMBER"]},"; when the phone answers it bridges the phone leg with a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["STREAM"]}," leg pointed at ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["WS_ENDPOINT"]},". Answer the phone and speak, and you should hear your own voice echoed back, while the server console prints frame/byte counts."]},{"$$mdtype":"Tag","name":"blockquote","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Order matters."]}," The server and ngrok must be running ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["before"]}," you trigger the call. If ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["WS_ENDPOINT"]}," points at a dead tunnel, the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["STREAM"]}," leg fails to connect and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onHangup"]}," tears the call down, so you'll hear nothing."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["(There is also ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["scripts/trigger-call.js"]},", the same payload in browser/Node ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["fetch"]}," form. Note: calling the Sinch API directly from a browser hits CORS, so proxy it through a backend in production.)"]},{"$$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":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Server console:"]}," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["New WebSocket connection ..."]},", then ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ConnectRequest — callId: ... appId: ..."]},", then ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Sent ConnectResponse: answer"]},", then periodic ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Audio frames: 100 | Bytes: ..."]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Your phone:"]}," you hear your own voice echoed back (Python server) or silence (the other servers, which only record)."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["API call:"]}," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["trigger-call.sh"]}," prints ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Call created successfully (HTTP 201)"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["On disk:"]}," a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call-<timestamp>.pcm"]}," file grows. Play it back to confirm encoding (see \"PCM format\" below)."]}]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"the-svaml-how-the-legs-bridge","__idx":12},"children":["The SVAML: how the legs bridge"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The outbound trigger sends this shape. Two ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["bridgeCall"]}," commands (same ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["bridgeName"]},") connect the phone leg and the stream leg so audio flows both ways. This mirrors the spec's own ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["dialToStream"]}," / echo examples."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"{\n  \"commands\": [\n    {\n      \"command\": \"dial\",\n      \"callName\": \"phone-leg\",\n      \"from\": { \"type\": \"PHONE\", \"phone\": { \"number\": \"+1SINCH_NUMBER\" } },\n      \"to\":   { \"type\": \"PHONE\", \"phone\": { \"number\": \"+1DESTINATION\" } },\n      \"dialTimeoutDurationSeconds\": 30,\n      \"maxCallDurationSeconds\": 1800,\n      \"events\": {\n        \"onAnswer\": [\n          { \"command\": \"bridgeCall\", \"bridgeName\": \"stream-bridge\" },\n          {\n            \"command\": \"dial\",\n            \"callName\": \"stream-leg\",\n            \"to\": {\n              \"type\": \"STREAM\",\n              \"stream\": {\n                \"endpoint\": \"wss://abc123.ngrok-free.app\",\n                \"streamOptions\": {\n                  \"version\":    1,\n                  \"codec\":      \"PCM\",\n                  \"sampleRate\": 8000\n                },\n                \"callHeaders\": [\n                  { \"key\": \"X-Tutorial\", \"value\": \"sinch-ws-agent\" }\n                ]\n              }\n            },\n            \"dialTimeoutDurationSeconds\": 10,\n            \"events\": {\n              \"onAnswer\": [{ \"command\": \"bridgeCall\", \"bridgeName\": \"stream-bridge\" }],\n              \"onHangup\": [{ \"command\": \"hangup\", \"callName\": \"phone-leg\" }]\n            }\n          }\n        ],\n        \"onHangup\": [{ \"command\": \"hangup\", \"callName\": \"stream-leg\" }]\n      }\n    }\n  ]\n}\n","lang":"json"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Use ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callHeaders"]}," (max 16 pairs, key/value up to 255 chars each) to pass identifying metadata your server can read on connect."]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"the-on-the-wire-websocket-protocol","__idx":13},"children":["The on-the-wire WebSocket protocol"]},{"$$mdtype":"Tag","name":"blockquote","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Heads up: not in the OpenAPI spec."]}," The spec defines the ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["SVAML"]}," shape (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["endpoint"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["streamOptions"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callHeaders"]},") but does ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["not"]}," document the messages exchanged over the WebSocket itself (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ConnectRequest"]},", the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["answer"]}," reply, ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["heartbeat"]},"/",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["StreamControl"]},"). The protocol below reflects the Streams product behavior the example servers implement; treat the exact JSON field names as ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["unverified against the spec"]}," and confirm them against the current Sinch Streams documentation before relying on them in production."]}]},{"$$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":"Format"},"children":["Format"]},{"$$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 to Server"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["JSON (text)"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ConnectRequest"]},": ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["{\"command\":\"connect\",\"callId\":\"…\",\"applicationId\":\"…\",\"headers\":{…}}"]}]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Server to Sinch"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["JSON (text)"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ConnectResponse"]},": ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["{\"command\":\"answer\"}"]}," (accept)."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Sinch and Server"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Binary"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Raw PCM audio frames (codec ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["PCM"]},", negotiated ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sampleRate"]},")."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Server to Sinch"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["JSON (text)"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Control messages such as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["{\"command\":\"heartbeat\"}"]}," during silence."]}]}]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The example servers all follow the same lifecycle: on the text ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["connect"]}," message they record ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callId"]},"/",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["applicationId"]},"/",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["headers"]},", reply ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["{\"command\":\"answer\"}"]},", then treat every binary frame as PCM audio and (optionally) write PCM back."]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"pcm-format","__idx":14},"children":["PCM format"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Codec:"]}," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["PCM"]}," only (the spec fixes ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["codec"]}," to the constant ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["PCM"]},"). If you need Opus or G.711-µ, transcode to PCM on your own gateway before the WebSocket hop."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Sample rate:"]}," one of ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["8000 | 16000 | 24000 | 44100 | 48000 | 96000"]}," Hz; default ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["8000"]},". For PSTN calls audio is typically sampled at 8 kHz, so a higher rate does not improve perceived quality on a PSTN-only path; it just increases bandwidth and processing load. Higher rates help on non-PSTN (SIP/streaming) paths or when a downstream STT model prefers wideband."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Sample encoding / framing:"]}," The spec says \"uncompressed raw audio\" but does ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["not"]}," state the bit depth, endianness, or channel count of each frame. The example servers treat frames as opaque bytes (write to file, echo back), which works regardless. ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Before decoding the PCM yourself, confirm the format."]}," It is almost certainly ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["16-bit signed little-endian, mono"]},", but this is ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["not stated in the OpenAPI spec"]},". To verify, play a recorded ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call-*.pcm"]}," with the assumed parameters, e.g.:",{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"ffplay -f s16le -ar 8000 -ac 1 call-<timestamp>.pcm\n","lang":"bash"},"children":[]},"Adjust ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["-ar"]}," to match the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sampleRate"]}," you negotiated. If it sounds correct, your assumptions hold."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Audio you send ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["back"]}," must match the negotiated ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["codec"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sampleRate"]},", in the same frame encoding."]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"sending-audio-back","__idx":15},"children":["Sending audio back"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Write raw PCM binary frames to the same connection. The Python server already does this (echo); to turn the other servers into echo servers, send the received binary frame straight back:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Node.js"]}," (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["scripts/ws-server.node.js"]},", in the binary branch): ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ws.send(data);"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["PHP"]}," (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["scripts/ws-server.php"]},", in the binary branch): ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["$from->send($msg);"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Java"]}," (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["scripts/WsServer.java"]},", in ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["onBinaryMessage"]},"): ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["session.getBasicRemote().sendBinary(ByteBuffer.wrap(bytes));"]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["In a real bot you'd replace the echo with: STT, then LLM, then TTS, then send the synthesized PCM back. Each server marks the spot with a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["--- HERE: plug in your AI / STT processing ---"]}," comment."]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"inbound-calls-the-ice-webhook-path","__idx":16},"children":["Inbound calls (the ICE webhook path)"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["To route calls that come ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["into"]}," your Sinch number to the stream, run the ICE webhook server instead of the outbound trigger."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"# Node.js\nnpm install express\nnode scripts/ice-callback.node.js     # listens on PORT (default 3001)\n\n# Python\npip install flask python-dotenv\npython scripts/ice-callback.py        # listens on PORT (default 8081)\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"blockquote","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Note:"]}," the two ice-callback scripts have ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["different"]}," built-in default ports (Node 3001, Python 8081). If ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["PORT"]}," is set in ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":[".env"]},", it overrides both. Pick one runtime and expose its port with a second ngrok tunnel; set your Sinch service webhook URL to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["https://<that-ngrok>.ngrok-free.app/webhook"]},"."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["On ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.incoming"]},", the server answers the inbound leg, bridges it, dials a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["STREAM"]}," leg to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["WS_ENDPOINT"]},", and bridges that into the same ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["stream-bridge"]},". When either side hangs up, the other is torn down."]},{"$$mdtype":"Tag","name":"blockquote","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Couldn't verify: inbound event shape."]}," The scripts read the caller number from ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["body.call.from.phone.number"]},". The spec's prose webhook example (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.answered"]},") shows a ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["flat"]}," shape (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["\"from\": \"+1234567890\""]},"), and the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.incoming"]}," payload is not formally schematized in the OpenAPI file. Log the raw request body once and confirm the actual nesting for your account before depending on ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["call.from.phone.number"]},"."]}]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"production-readiness-checklist","__idx":17},"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":["Authenticate inbound WS"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["endpoint"]}," is open by default. Embed a token query param (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["?token=…"]},") or pass it via ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["callHeaders"]},", then validate on ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ConnectRequest"]},"."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Heartbeat / keepalive"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The example servers send ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["{\"command\":\"heartbeat\"}"]}," every 5 s; tune to keep proxies alive during long silences."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Backpressure"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["If you can't process incoming audio fast enough, ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["drop"]}," frames rather than buffer indefinitely. Buffering inflates latency."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Disconnect handling"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["When the caller hangs up, Sinch closes the WS. Handle the close (free STT/TTS sessions, flush PCM files). All four servers do this in their close handler."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Partial / non-JSON frames"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Each server tolerates a non-JSON text frame and treats anything binary as audio; verify your STT layer is robust to short final frames."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Logging & PCAP"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Capture the JSON control frames and a sample of binary frames during dev; most bugs are \"which side stopped sending audio first.\""]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Codec"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Only ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["PCM"]}," is supported. Terminate other codecs on your gateway and re-encode to PCM before the WS hop."]}]}]}]}]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"language-coverage","__idx":18},"children":["Language coverage"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["All files live under ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"https://github.com/sinch/sinch-voice-tutorials/tree/main/4.2-stream-audio/scripts"},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["4.2-stream-audio/scripts/"]}]}," in the 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":"Runtime"},"children":["Runtime"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Echoes by default?"},"children":["Echoes by default?"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"https://github.com/sinch/sinch-voice-tutorials/blob/main/4.2-stream-audio/scripts/ws-server.py"},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ws-server.py"]}]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Python (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["websockets"]},")"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Yes"]}," (use this for the first-success demo)"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"https://github.com/sinch/sinch-voice-tutorials/blob/main/4.2-stream-audio/scripts/ws-server.node.js"},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ws-server.node.js"]}]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Node.js (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ws"]},")"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["No (records + heartbeats)"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"https://github.com/sinch/sinch-voice-tutorials/blob/main/4.2-stream-audio/scripts/ws-server.php"},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ws-server.php"]}]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["PHP (Ratchet)"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["No (records)"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"https://github.com/sinch/sinch-voice-tutorials/blob/main/4.2-stream-audio/scripts/WsServer.java"},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["WsServer.java"]}]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Java (Tyrus)"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["No (records + heartbeats)"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"https://github.com/sinch/sinch-voice-tutorials/blob/main/4.2-stream-audio/scripts/ice-callback.node.js"},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ice-callback.node.js"]}]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Node/Express"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Inbound webhook"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"https://github.com/sinch/sinch-voice-tutorials/blob/main/4.2-stream-audio/scripts/ice-callback.py"},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ice-callback.py"]}]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Python/Flask"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Inbound webhook"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"https://github.com/sinch/sinch-voice-tutorials/blob/main/4.2-stream-audio/scripts/trigger-call.sh"},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["trigger-call.sh"]}]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["bash + curl"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Outbound trigger"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"https://github.com/sinch/sinch-voice-tutorials/blob/main/4.2-stream-audio/scripts/trigger-call.js"},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["trigger-call.js"]}]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["browser/Node fetch"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Outbound trigger"]}]}]}]}]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"some-specification-details","__idx":19},"children":["Some specification details"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["STREAM"]}," is one of ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["to"]},"'s three discriminator values (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["PHONE"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["STREAM"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["VOICE_RELAY"]},")."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["stream.endpoint"]}," is ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["required"]},"; it must be a valid ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ws://"]}," or ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["wss://"]}," URL reachable from the public internet."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["stream.streamOptions.codec"]}," is the constant ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["PCM"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["stream.streamOptions.sampleRate"]}," is one of ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["{8000, 16000, 24000, 44100, 48000, 96000}"]},", default ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["8000"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["stream.streamOptions.version"]}," defaults to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["1"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["stream.callHeaders[]"]}," allows up to 16 ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["{key, value}"]}," pairs, each field up to 255 chars."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["The ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["WebSocket wire protocol"]}," (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ConnectRequest"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["answer"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["heartbeat"]},") and the ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["PCM sample encoding"]}," (bit depth/endianness/channels) are ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["not"]}," in the OpenAPI spec; verify them against the Sinch Streams product docs."]}]}]},"headings":[{"value":"Stream Call Audio in Real-Time","id":"stream-call-audio-in-real-time","depth":1},{"value":"Overview","id":"overview","depth":2},{"value":"Setup (do this once)","id":"setup-do-this-once","depth":2},{"value":"Get the code","id":"get-the-code","depth":3},{"value":"Credentials and .env","id":"credentials-and-env","depth":3},{"value":"What about ice-callback ?","id":"what-about-ice-callback-","depth":3},{"value":"Prerequisites","id":"prerequisites","depth":3},{"value":"First success: hear your own voice echoed back","id":"first-success-hear-your-own-voice-echoed-back","depth":2},{"value":"1. Start the echo WebSocket server","id":"1-start-the-echo-websocket-server","depth":3},{"value":"2. Expose it with ngrok","id":"2-expose-it-with-ngrok","depth":3},{"value":"3. Trigger the call","id":"3-trigger-the-call","depth":3},{"value":"What success looks like","id":"what-success-looks-like","depth":3},{"value":"The SVAML: how the legs bridge","id":"the-svaml-how-the-legs-bridge","depth":2},{"value":"The on-the-wire WebSocket protocol","id":"the-on-the-wire-websocket-protocol","depth":2},{"value":"PCM format","id":"pcm-format","depth":2},{"value":"Sending audio back","id":"sending-audio-back","depth":2},{"value":"Inbound calls (the ICE webhook path)","id":"inbound-calls-the-ice-webhook-path","depth":2},{"value":"Production-readiness checklist","id":"production-readiness-checklist","depth":2},{"value":"Language coverage","id":"language-coverage","depth":2},{"value":"Some specification details","id":"some-specification-details","depth":2}],"frontmatter":{"seo":{"title":"Stream Call Audio in Real-Time"}},"lastModified":"2026-08-31T13:48:15.000Z","pagePropGetterError":{"message":"","name":""}},"slug":"/docs/voice-2.0/tutorials/stream-audio","userData":{"isAuthenticated":false,"teams":["anonymous"]},"isPublic":true}