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
| 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.
Four steps, all through the API or the dashboard
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.
POST /v1/stream-test with your stream_url. Fix whatever step fails before you place a single call.
For inbound calls, PUT /v1/phone-numbers/{id}/inbound with target media_stream. For outbound, POST /v1/calls with stream_url.
GET /v1/calls/{sid}/events shows call.stream.connected or call.stream.failed with the reason, so you know whether Edesy reached your bot.
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.
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.
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"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"}
}'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 TTSYou own the audio pipeline; Edesy owns the phone call
16-bit little-endian mono frames. Your serializer only moves bytes, and resamples if your pipeline runs at a different rate.
Caller, called number, direction, call SID and your custom parameters arrive before the first audio frame.
killAudio flushes what your bot was saying the moment the caller talks over it. disconnect ends the call.
DTMF digits arrive as text frames, so the bot can mix speech and keypad input.
stream-test reports which step failed (DNS, TCP, TLS, upgrade or audio) and refuses addresses that aren't on the public internet.
call.stream.connected, failed, started and stopped on the call's event timeline, next to ringing and answered.
See the full guide: choosing SIP, stream or relay
You run speech recognition, speech synthesis, voice activity detection and barge-in. Best when you already have a tuned pipeline.
Edesy runs the speech side and your bot exchanges text only. Best when you have an LLM but no audio pipeline.
Your platform already speaks SIP, such as LiveKit, Vapi, Retell or Bolna. Edesy sends the call over a SIP trunk.
Explore more comparisons, alternatives, and resources
Tell us about your stack and expected call volume. We'll help you get the first call streaming.
Create an API key, run the stream test, and point a number at your WebSocket.