WebSocket audio streaming connects a live phone call to your own voice bot: the platform opens a WebSocket to your server, sends the caller's audio as it arrives, and plays back the audio you send. On Edesy it is raw L16 PCM, mono, 8 or 16 kHz, in binary frames, with JSON text frames for metadata and control.
Stream a call to your bot: Open the Edesy voice portal. Buy a number and point it at your
wss://URL.
Three ways to put a bot on a phone call
| Approach | Your bot receives | You run | Good for |
|---|---|---|---|
| Media Streams (WebSocket audio) | Raw audio | STT, LLM, TTS, VAD | Full control; Pipecat and custom pipelines |
| Conversation Relay | Text | LLM only | Text-first bots; fastest to build |
| SIP | A SIP call | A SIP-capable agent platform | LiveKit Cloud, Vapi, Retell, Bolna |
This post is about the first. For text bots, see Conversation Relay for Indian numbers.
The connection lifecycle
- The call connects. For inbound calls, the number's route is a media stream. For outbound, you passed
stream_urltoPOST /v1/calls. - Edesy opens the WebSocket to your
wss://URL. - One text frame arrives first: JSON metadata with
sampleRate,mixType,callSid,from,toandcustomerData(yourcustom_parameters). - Binary frames flow both ways. Caller audio comes in; your bot's audio goes out.
- Control frames go either way as JSON text.
- The call ends and the socket closes, or your bot sends
disconnect.
Stream events are recorded on the call: call.stream.connected, call.stream.failed, call.stream.started and
call.stream.stopped. You can see them at GET /v1/calls/:sid/events.
The frames
{"sampleRate": 16000, "mixType": "mono", "callSid": "...", "from": "+9199...",
"to": "+9180...", "customerData": {"assistant_id": "asst_456"}}| Frame | Direction | Type | Meaning |
|---|---|---|---|
| Metadata | Edesy to bot | Text (once) | Call details and your custom parameters |
| Audio | Edesy to bot | Binary | Caller audio, L16 PCM |
| Audio | Bot to Edesy | Binary | Audio to play to the caller |
{"type":"killAudio"} |
Bot to Edesy | Text | Flush queued playback (barge-in) |
{"type":"disconnect"} |
Bot to Edesy | Text | Hang up |
{"event":"dtmf","dtmf":"5"} |
Edesy to bot | Text | Caller pressed a key |
Why raw PCM matters
Many streaming APIs send base64-encoded mu-law audio inside JSON messages. That works, but every frame must be JSON-parsed, base64-decoded and mu-law expanded before your STT sees it, and the reverse on the way out. Raw L16 PCM in binary frames skips all three steps. Most STT engines accept 16-bit PCM directly, and most TTS engines can output it.
If your pipeline works in 20 ms chunks, the sizes are simple: at 16 kHz, 20 ms is 320 samples, or 640 bytes; at 8 kHz it is 160 samples, or 320 bytes. Don't assume incoming frames are always exactly one chunk; buffer and slice.
Picking a sample rate
| Rate | When to choose it |
|---|---|
| 16 kHz | Your STT and TTS run at 16 kHz; you want to avoid resampling |
| 8 kHz | Your pipeline is built for narrowband telephony audio |
Set the rate with sample_rate on the number's inbound route or stream_sample_rate on an outbound call. A
mismatch is the most common cause of audio that sounds too fast, too slow or robotic. Read sampleRate from the
metadata frame rather than hard-coding it.
Barge-in: let the caller interrupt
Phone callers interrupt. If the bot keeps talking, the call feels broken. The pattern:
- Keep a VAD running on incoming audio while the bot speaks.
- When it hears speech, send
{"type":"killAudio"}so queued playback stops. - Cancel the in-flight LLM and TTS work for the old reply.
- Trim your conversation history to what the caller actually heard.
Step 4 is easy to forget. If the bot "said" three sentences in your history but the caller heard one, later replies drift.
Keypresses
Callers still press keys, especially for "press 1 to talk to a person". DTMF arrives as a text frame with the digit. Treat it as an input event in your bot, the same way you treat a transcript.
Before you go live
- Preflight the URL:
POST /v1/stream-testwithstream_url,sample_rateandsecondsconnects from Edesy's side and reports DNS, TCP, TLS, upgrade and audio stages. - Accept the upgrade quickly and authenticate during the upgrade.
- Handle the metadata frame before audio; a parser that expects binary first will crash.
- Log
callSidon every line so you can match bot logs to the call timeline. - Use
custom_parametersto pass the tenant, campaign or customer ID.
Inbound and outbound with the same bot
Inbound: PUT /v1/phone-numbers/:id/inbound with {"target":"media_stream","stream_url":"wss://...","sample_rate":16000}.
Outbound: POST /v1/calls with stream_url and stream_sample_rate. The bot code doesn't change.
If you use Pipecat, the EdesyFrameSerializer sample implements this protocol; see
connect a Pipecat bot to an Indian number.
Cost
The call is billed at Rs 0.50 per minute on a 60-second pulse, unanswered calls free, plus number rental (Rs 499 per month for mobile or 92-series). Prices exclude 18% GST. See Voice API pricing.
Read more on the Media Streams page, the full list of inbound targets on call routing, and the protocol reference in the Voice API docs.