To migrate Twilio Calls API code to Edesy, change the base URL to https://voice-api.edesy.in/v1, swap in your
Edesy Account SID and Auth Token (or an API key), send the request as JSON with snake_case fields, and rewrite
each TwiML answer handler to return VoiceML JSON. Status callbacks keep Twilio's PascalCase field names, so that
handler barely changes.
Start the migration: Open the Edesy voice portal. Complete KYC, buy a number and create an API key.
If you are still deciding whether to move, read the honest Twilio comparison first. This post assumes you have decided to try it.
Before you start
- Sign up on voice-app.edesy.in and complete KYC (PAN and Aadhaar; review takes about 24 hours).
- Top up the INR wallet with Razorpay.
- Buy an Indian number (mobile, 92-series, landline or toll-free) to use as the caller ID.
- Create an API key, or note the Account SID and Auth Token.
Step 1: Change the base URL and auth
| Twilio | Edesy | |
|---|---|---|
| Base URL | https://api.twilio.com/2010-04-01 |
https://voice-api.edesy.in/v1 |
| Create call | POST /Accounts/{Sid}/Calls.json |
POST /Accounts/{Sid}/Calls or POST /calls |
| Auth | Basic AccountSid:AuthToken |
Basic AccountSid:AuthToken, or Authorization: Bearer <api key> |
| Body | form-encoded | JSON |
Step 2: Map the request fields
| Twilio parameter | Edesy field | Notes |
|---|---|---|
To |
to |
E.164, e.g. +9198XXXXXXXX |
From |
from |
An Edesy number you own |
Url |
url |
Must return VoiceML JSON |
Method |
method |
GET or POST |
Twiml |
voiceml |
Inline VoiceML as a JSON string instead of inline TwiML |
StatusCallback |
status_callback |
Signed and retried |
StatusCallbackMethod |
status_method |
GET or POST |
Timeout |
timeout |
Ring timeout, 5 to 600 seconds |
MachineDetection |
none on single calls | Use a campaign for answering-machine detection |
ApplicationSid |
application_id |
application_sid is accepted as an alias |
| (none) | text |
Speak a message with no webhook at all |
| (none) | stream_url |
Send call audio to your WebSocket bot |
| (none) | custom_parameters |
Echoed back in webhooks |
A minimal call, in Node with fetch:
const res = await fetch("https://voice-api.edesy.in/v1/calls", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.EDESY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
from: "+9180XXXXXXXX",
to: "+9198XXXXXXXX",
url: "https://your-app.example.com/answer",
status_callback: "https://your-app.example.com/status",
custom_parameters: { order_id: "A-1042" },
}),
});Step 3: Convert TwiML to VoiceML
This is the main job. VoiceML is a JSON object with an instructions array, and each item has a verb.
| TwiML | VoiceML verb | Common fields |
|---|---|---|
<Say> |
say |
text, voice, language, loop |
<Play> |
play |
url, loop, digits |
<Gather> |
gather |
keypad digits only; num_digits, timeout, finish_on_key, nested (for speech, use Conversation Relay or Media Streams) |
<Record> |
record |
action, max_length, play_beep, transcribe |
<Dial><Number> |
dial |
number, caller_id, timeout, time_limit, record, answer_on_bridge |
<Stream> |
stream |
url, custom_parameters |
<Redirect> |
redirect |
url, method |
<Enqueue> |
not supported | Route the number to a contact-center queue instead |
<Reject> |
reject |
reason (rejected, busy) |
<Hangup> |
hangup |
|
<Pause> |
pause |
length |
Before (TwiML):
<Response>
<Gather numDigits="1" action="/menu">
<Say>Press 1 to confirm your appointment, or 2 to reschedule.</Say>
</Gather>
<Say>Sorry, we did not get that.</Say>
</Response>After (VoiceML):
{
"instructions": [
{ "verb": "gather", "input": "dtmf", "num_digits": 1, "action": "https://your-app.example.com/menu",
"nested": [ { "verb": "say", "text": "Press 1 to confirm your appointment, or 2 to reschedule." } ] },
{ "verb": "say", "text": "Sorry, we did not get that." }
]
}Edesy does not parse TwiML, so every answer URL and every action URL in your flows needs this change. A clean
way to do it is to keep your flow logic and replace only the response builder.
Step 4: Update the status callback handler
Edesy posts Twilio-style fields: CallSid, AccountSid, From, To, CallStatus, Direction, Duration and
Timestamp. CallStatus values are the familiar ones: initiated, ringing, in-progress, completed, busy, failed,
no-answer and canceled.
Three changes to make:
- Parse JSON. POST callbacks arrive as a JSON body (GET callbacks put the same fields in the query string).
- Verify the signature. Edesy signs each delivery with HMAC-SHA256 in
X-Webhook-Signature(v1=<hex>) andX-Webhook-Timestamp. The signed message is<timestamp>.<raw body>. - Use the extra fields.
SipResponseCode,HangupReasonandCallOutcometell you why a call ended. See call status callbacks explained for the full tables.
Step 5: Move streaming and relay bots
- ConversationRelay bots move unchanged. Point an Edesy number at the bot's WebSocket using the Conversation Relay inbound target.
- Media Streams bots need a new serializer. Edesy sends raw L16 PCM in binary frames, not base64 mu-law in JSON. See WebSocket audio streaming, or the Pipecat guide if you use Pipecat.
Step 6: Test and cut over
- Run each flow against a real phone from the portal's "Make a call" screen.
- Check the per-call event timeline:
GET /v1/calls/:sid/events. - Check the debugger and webhook delivery log; replay any failed delivery.
- Move traffic gradually. If you want to keep a Twilio Elastic SIP trunk as a carrier during the move, use the BYOC recipe on SIP trunking.
Cost after the move
Voice API calls cost Rs 0.50 per minute on a 60-second pulse, and unanswered calls are free. Numbers are Rs 499 per month (mobile or 92-series), Rs 449 (landline) or Rs 999 (toll-free). All prices exclude 18% GST. See Voice API pricing.
The Voice API docs have the full field reference and the VoiceML verb reference. For an overview, see the Voice API hub and Twilio alternative for India.