Part of the Edesy Business Telephony Suite
Media Streams

Stream Phone Call Audio to Your WebSocket

Edesy Media Streams sends a live phone call's audio to your own WebSocket and plays back whatever your bot returns. Audio is raw L16 PCM, mono, at 8 or 16 kHz, after one JSON metadata frame. Use it to run your own voice bot, such as a Pipecat pipeline, on Indian numbers at Rs 0.50/min.

L16 PCM

No base64, no mu-law

8 / 16 kHz

Mono, both ways

Rs 0.50

Per minute, 60 s pulse

1 call

To point a number at your bot

Bring your own bot

Inbound and outbound

Preflight test endpoint

Works with Pipecat

Media Streams spec

Transport One WebSocket per call (wss:// in production), opened by Edesy to your server
Audio format Raw L16 PCM: signed 16-bit little-endian, mono
Sample rate 16000 Hz (default) or 8000 Hz, set per number or per call
Direction Bidirectional: caller audio in, your bot's audio out, as binary frames
First frame One JSON text frame with call metadata and your custom parameters
Control {"type":"killAudio"} flushes playback (barge-in), {"type":"disconnect"} hangs up
Keypad {"event":"dtmf","dtmf":"5"} arrives as a text frame
Inbound setup PUT /v1/phone-numbers/{id}/inbound with target: "media_stream"
Outbound setup POST /v1/calls with stream_url (and optional stream_sample_rate)
Preflight POST /v1/stream-test: DNS, TCP, TLS, upgrade and audio checks, no call needed
Call events call.stream.connected, call.stream.failed, call.stream.started, call.stream.stopped
Price Rs 0.50/min, 60-second pulse, unanswered calls free, plus number rental. You pay your own speech and LLM vendors

Prices exclude 18% GST.

From WebSocket to live phone calls

Four steps, all through the API or the dashboard

  1. Step 1 of 4: Run a WebSocket server

    Accept the connection, read the first text frame as JSON metadata, then read and write binary L16 frames. The Pipecat sample does this for you.

  2. Step 2 of 4: Run the preflight

    POST /v1/stream-test with your stream_url. Fix whatever step fails before you place a single call.

  3. Step 3 of 4: Point a number or place a call

    For inbound calls, PUT /v1/phone-numbers/{id}/inbound with target media_stream. For outbound, POST /v1/calls with stream_url.

  4. Step 4 of 4: Watch the call events

    GET /v1/calls/{sid}/events shows call.stream.connected or call.stream.failed with the reason, so you know whether Edesy reached your bot.

The wire protocol

1. Metadata frame. As soon as your WebSocket accepts, Edesy sends one text frame with the call details. customerData carries the call_sid returned by POST /v1/calls and any custom parameters.

{
  "sampleRate": 16000,
  "mixType": "mono",
  "callSid": "...",
  "from": "+9198XXXXXXXX",
  "to": "+9180XXXXXXXX",
  "direction": "inbound",
  "customerData": { "call_sid": "CA_...", "order_id": "A123" }
}

2. Audio, both ways. After that, binary frames carry raw L16 PCM at sampleRate. Frames from Edesy are the caller's voice; frames you send are played to the caller. There is no base64 and no mu-law to decode.

3. Control, as text frames. Send {"type":"killAudio"} when the caller interrupts, to drop audio that hasn't played yet, and {"type":"disconnect"} to end the call. Keypresses reach you as {"event":"dtmf","dtmf":"5"}.

The same protocol is used for inbound and outbound calls, so one bot serves both.

Check your endpoint before a real call

curl -X POST https://voice-api.edesy.in/v1/stream-test \
  -H "Authorization: Bearer $EDESY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"stream_url": "wss://bot.example.com/ws", "sample_rate": 16000, "seconds": 2}'

Edesy connects from the same network its media servers use, streams a few seconds of silence, and returns a checklist (dns, tcp, tls, upgrade, audio) with a plain-language hint for the step that failed, plus how many audio and text frames your bot sent back.

Inbound: route a number to your bot

One request binds the number. Edesy provisions the telephony behind it; there is no SIP or media server for you to configure.

# Point an Indian number at your bot (inbound calls)
curl -X PUT https://voice-api.edesy.in/v1/phone-numbers/$PHONE_NUMBER_ID/inbound \
  -H "Authorization: Bearer $EDESY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"target": "media_stream", "stream_url": "wss://bot.example.com/ws", "sample_rate": 16000}'

# Detach it again
curl -X DELETE https://voice-api.edesy.in/v1/phone-numbers/$PHONE_NUMBER_ID/inbound \
  -H "Authorization: Bearer $EDESY_API_KEY"

Outbound: stream an answered call

The same bot handles outbound calls. custom_parameters arrive in the metadata frame and are echoed on your status callback.

# Dial out and stream the answered call to your bot
curl -X POST https://voice-api.edesy.in/v1/calls \
  -H "Authorization: Bearer $EDESY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "+9180XXXXXXXX",
    "to":   "+9198XXXXXXXX",
    "stream_url": "wss://bot.example.com/ws",
    "stream_sample_rate": 16000,
    "status_callback": "https://myapp.example.com/call-status",
    "custom_parameters": {"order_id": "A123"}
  }'

Pipecat: EdesyFrameSerializer

The pipecat-edesy sample has a ready serializer, a FastAPI /ws server and a bot using Deepgram STT, an OpenAI LLM, Cartesia TTS and Silero VAD. Swap any vendor.

# Pipecat: the pipecat-edesy sample, trimmed to the Edesy-specific parts
from edesy_serializer import EdesyFrameSerializer   # copy from the sample repo

@app.websocket("/ws")
async def ws(websocket: WebSocket):
    await websocket.accept()
    metadata = json.loads(await websocket.receive_text())   # 1. metadata frame
    sample_rate = int(metadata.get("sampleRate", 16000))

    serializer = EdesyFrameSerializer(
        metadata=metadata,
        params=EdesyFrameSerializer.InputParams(edesy_sample_rate=sample_rate),
    )
    transport = FastAPIWebsocketTransport(
        websocket=websocket,
        params=FastAPIWebsocketParams(
            audio_in_enabled=True,
            audio_out_enabled=True,
            add_wav_header=False,
            vad_analyzer=SileroVADAnalyzer(),
            serializer=serializer,
        ),
    )
    # 2. then a normal pipeline: Deepgram STT -> OpenAI LLM -> Cartesia TTS

Built for people running their own speech stack

You own the audio pipeline; Edesy owns the phone call

Plain linear PCM

16-bit little-endian mono frames. Your serializer only moves bytes, and resamples if your pipeline runs at a different rate.

Metadata on connect

Caller, called number, direction, call SID and your custom parameters arrive before the first audio frame.

Barge-in and hang-up

killAudio flushes what your bot was saying the moment the caller talks over it. disconnect ends the call.

Keypad presses

DTMF digits arrive as text frames, so the bot can mix speech and keypad input.

Preflight checklist

stream-test reports which step failed (DNS, TCP, TLS, upgrade or audio) and refuses addresses that aren't on the public internet.

Per-call stream events

call.stream.connected, failed, started and stopped on the call's event timeline, next to ringing and answered.

Media Streams, Conversation Relay or SIP?

See the full guide: choosing SIP, stream or relay

Media Streams: FAQ

Putting your own voice bot on Indian numbers?

Tell us about your stack and expected call volume. We'll help you get the first call streaming.

Your bot. Our phone lines.

Create an API key, run the stream test, and point a number at your WebSocket.