# WebSocket Audio Streaming for Voice Bots: How It Works (2026)

> How to stream live phone call audio to your own voice bot over a WebSocket: the connection lifecycle, raw L16 PCM frames, barge-in, DTMF, sample rates, and how it compares with base64 mu-law streams.

Source: https://edesy.in/blog/websocket-audio-streaming-voice-bot-2026

**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](https://voice-app.edesy.in/?utm_source=edesy.in&utm_medium=blog&utm_campaign=websocket-audio-streaming-voice-bot-2026). 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](https://edesy.in/blog/conversation-relay-text-voice-bot-india-2026).

## The connection lifecycle

1. **The call connects.** For inbound calls, the number's route is a media stream. For outbound, you passed
   `stream_url` to `POST /v1/calls`.
2. **Edesy opens the WebSocket** to your `wss://` URL.
3. **One text frame arrives first:** JSON metadata with `sampleRate`, `mixType`, `callSid`, `from`, `to` and
   `customerData` (your `custom_parameters`).
4. **Binary frames flow both ways.** Caller audio comes in; your bot's audio goes out.
5. **Control frames** go either way as JSON text.
6. **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

```json
{"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:

1. Keep a VAD running on incoming audio while the bot speaks.
2. When it hears speech, send `{"type":"killAudio"}` so queued playback stops.
3. Cancel the in-flight LLM and TTS work for the old reply.
4. 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-test` with `stream_url`, `sample_rate` and `seconds` connects 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 `callSid`** on every line so you can match bot logs to the call timeline.
- **Use `custom_parameters`** to 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](https://edesy.in/blog/connect-pipecat-bot-to-indian-phone-number-2026).

## 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](https://edesy.in/voice-api/pricing).

Read more on the [Media Streams page](https://edesy.in/voice-api/media-streams), the full list of inbound targets on
[call routing](https://edesy.in/call-routing), and the protocol reference in the [Voice API docs](https://edesy.in/docs/voice-api).

## Frequently Asked Questions

### What is WebSocket audio streaming for a phone call?

The voice platform opens a WebSocket to your server when a call connects and sends the caller's audio in real time. Your server sends audio back, which the caller hears. It lets you run your own speech recognition, LLM and text-to-speech on a real phone call.

### What audio format does Edesy stream?

Raw L16 PCM: signed 16-bit little-endian, mono, at 8 kHz or 16 kHz, sent as binary WebSocket frames in both directions. It is not base64 and not mu-law.

### Should I use 8 kHz or 16 kHz?

Use 16 kHz if your STT and TTS work at 16 kHz, so you avoid an extra resample. Use 8 kHz if your pipeline is built for narrowband telephony audio. The rate on the number or call must match what your bot expects.

### How do I handle the caller interrupting the bot?

When your voice activity detection hears the caller while the bot is speaking, send a text frame {"type":"killAudio"}. Edesy flushes the audio still queued for playback, and the caller stops hearing the old reply.

### How do I end the call from the bot?

Send {"type":"disconnect"} as a text frame. You can also hang up through the REST API with DELETE /v1/calls/:sid.

### When should I use Conversation Relay instead?

When you don't want to run STT and TTS yourself. Conversation Relay sends your bot text and speaks the text you send back. Media Streams gives you raw audio and full control of the pipeline.
