To connect a Pipecat bot to an Indian phone number, buy a number on Edesy, run your bot with the
EdesyFrameSerializer, expose it over wss://, and call PUT /v1/phone-numbers/:id/inbound with
target: "media_stream" and your stream_url. Every call to that number then streams raw L16 audio to your bot,
and your bot's audio plays back to the caller.
Get a number for your bot: Open the Edesy voice portal. Complete KYC, buy a number and create an API key.
What you need
| Item | Where it comes from |
|---|---|
| An Indian number | Edesy portal, after KYC (PAN and Aadhaar; about 24 hours) |
| An API key | Edesy portal, API keys |
| A Pipecat bot | The pipecat-edesy sample, or your own pipeline |
| STT, LLM and TTS keys | Your vendors (the sample uses Deepgram, OpenAI and Cartesia) |
A public wss:// URL |
ngrok or similar for development; your load balancer in production |
How the pieces fit
caller --phone network--> Edesy --wss:// (L16 PCM)--> your Pipecat bot
^ |
+---------- audio back ---------+Edesy handles the phone call. Your bot handles listening, thinking and speaking. You never configure a SIP stack.
Step 1: Run the sample bot
The sample has three files that matter:
edesy_serializer.py: theEdesyFrameSerializer, which speaks Edesy's WebSocket protocol. Copy it into your project.bot.py: the Pipecat pipeline (Deepgram STT, OpenAI LLM, Cartesia TTS, Silero VAD). Swap any vendor.server.py: a FastAPI/wsendpoint that accepts the call, reads the metadata frame and runs the bot.
python -m venv venv && source venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # add your Deepgram, OpenAI and Cartesia keys
python server.py # listens on :7860, WebSocket path /wsStep 2: Expose it over TLS
Edesy connects to wss:// URLs. In development, ngrok http 7860 gives you a wss://...ngrok.app/ws address. In
production, terminate TLS at your load balancer. Accept the WebSocket upgrade quickly and do any auth at the
upgrade, not after it.
Step 3: Preflight the URL
Before you place a call, check that Edesy can reach the bot:
curl -X POST https://voice-api.edesy.in/v1/stream-test \
-H "Authorization: Bearer <your-api-key>" \
-H "Content-Type: application/json" \
-d '{"stream_url":"wss://your-host/ws","sample_rate":16000,"seconds":2}'The response is a checklist by stage (DNS, TCP, TLS, upgrade, audio), so a broken tunnel or a non-WebSocket route shows up by name.
Step 4: Point the number at the bot
curl -X PUT https://voice-api.edesy.in/v1/phone-numbers/<PHONE_NUMBER_ID>/inbound \
-H "Authorization: Bearer <your-api-key>" \
-H "Content-Type: application/json" \
-d '{"target":"media_stream","stream_url":"wss://your-host/ws","sample_rate":16000}'Get <PHONE_NUMBER_ID> from GET /v1/phone-numbers. sample_rate is 16000 or 8000 and must match your
pipeline; the sample reads it from the call metadata, so it stays in sync. Call the number and your bot answers.
To detach, send DELETE to the same path.
You can do the same thing in the portal: open the number, choose "Media stream" under "When a call comes in", and paste the URL.
Step 5: Make outbound calls
The same bot works for outbound. Pass stream_url when you create the call:
curl -X POST https://voice-api.edesy.in/v1/calls \
-H "Authorization: Bearer <your-api-key>" \
-H "Content-Type: application/json" \
-d '{"from":"+9180XXXXXXXX","to":"+9198XXXXXXXX",
"stream_url":"wss://your-host/ws","stream_sample_rate":16000,
"custom_parameters":{"assistant_id":"asst_456"}}'custom_parameters arrive in the metadata frame, so the bot knows which assistant, campaign or customer the
call is for without a lookup.
The wire protocol in short
| Direction | Frame | Content |
|---|---|---|
| Edesy to bot | Text, once on connect | JSON metadata: sampleRate, callSid, from, to, customerData |
| Edesy to bot | Binary | Caller audio, L16 PCM mono |
| Bot to Edesy | Binary | Audio to play to the caller |
| Bot to Edesy | Text | {"type":"killAudio"} to stop playback (barge-in), {"type":"disconnect"} to hang up |
| Edesy to bot | Text | {"event":"dtmf","dtmf":"5"} for a keypress |
For more on frame sizes, barge-in and debugging, see WebSocket audio streaming for voice bots.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Call connects, bot never gets a connection | URL not reachable from the internet | Run /v1/stream-test; check the tunnel |
| Bot crashes at the start of the call | Metadata frame not handled | Read the first text frame before audio |
| Robotic or chipmunk audio | Sample rate mismatch | Use the same rate on the number and in the pipeline |
| Bot talks over the caller | Barge-in not wired | Send killAudio when VAD hears the caller |
| Outbound call never reaches the bot | Request had no stream_url or application_id |
Add one |
Stream events (call.stream.connected, failed, started, stopped) also appear on the call's timeline at
GET /v1/calls/:sid/events.
Costs
Edesy charges Rs 0.50 per minute for the call (60-second pulse, unanswered calls free) plus number rental: Rs 499 per month for mobile or 92-series, Rs 449 for landline. Prices exclude 18% GST. STT, LLM and TTS are billed by your vendors. See Voice API pricing.
Media Streams, Conversation Relay or SIP?
If you'd rather not run STT and TTS yourself, Conversation Relay sends your bot text instead of audio. If your agent runs on LiveKit Cloud, Vapi, Retell or Bolna, a SIP route on SIP trunking may be simpler. All three are options on call routing. The Media Streams page and the Voice API docs have the full reference.