Most Twilio Programmable Voice code ports with small changes. Edesy accepts Twilio-style Basic auth (Account SID and Auth Token), serves Twilio-shaped paths such as /v1/Accounts/{AccountSid}/Calls, and sends status callbacks with the same PascalCase fields. The one real difference is the instruction format: answer URLs return VoiceML (JSON), not TwiML (XML), and Edesy does not parse TwiML.
Endpoint mapping#
| Twilio | Edesy | Notes |
|---|---|---|
POST/2010-04-01/Accounts/{Sid}/Calls.json |
POST/v1/Accounts/{Sid}/Calls or POST/v1/calls |
JSON body, not form-encoded |
GET.../Calls.json |
GET/v1/calls |
Filters: status, direction, from, to, start_date, end_date |
GET.../Calls/{CallSid}.json |
GET/v1/calls/{CallSid} |
Response has sid and call_sid |
POST.../Calls/{CallSid}.json with Status=completed |
POST/v1/calls/{CallSid} with {"status":"completed"}, or DELETE/v1/calls/{CallSid} |
See Update or End a Call |
GET.../Calls/{CallSid}/Recordings.json |
GET/v1/calls/{CallSid}/recordings or /v1/Accounts/{Sid}/Calls/{CallSid}/Recordings |
|
GET.../Recordings.json |
GET/v1/recordings or /v1/Accounts/{Sid}/Recordings |
Media via signed, expiring links |
POST.../Calls/{CallSid}/Recordings.json |
POST/v1/calls/{CallSid}/recordings/start |
|
| Call Notifications / Events | GET/v1/calls/{CallSid}/events |
Event timeline |
IncomingPhoneNumbers (list, update VoiceUrl) |
GET/v1/phone-numbers, PATCH/v1/phone-numbers/{id}, PUT/v1/phone-numbers/{id}/inbound |
See Inbound Routing |
AvailablePhoneNumbers + buy |
GET/v1/dids/available, POST/v1/dids/{id}/purchase |
Indian numbers, KYC first |
Applications (ApplicationSid) |
/v1/applications; application_id (or application_sid) on the call |
|
<Connect><Stream> Media Streams |
stream_url on the call, or the media_stream inbound target |
Different wire format, see below |
<Connect><ConversationRelay> |
conversation_relay inbound target |
Same message protocol |
Authentication#
# Twilio
curl -u "$TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKEN" https://api.twilio.com/2010-04-01/Accounts/$TWILIO_ACCOUNT_SID/Calls.json
# Edesy
curl -u "$ACCOUNT_SID:$AUTH_TOKEN" https://voice-api.edesy.in/v1/Accounts/$ACCOUNT_SID/Calls
Your Account SID is your workspace ID; the Auth Token is on the credentials card of the portal dashboard. For new code, a Bearer API key (Authorization: Bearer sk_...) is simpler. See Authentication.
Request parameters#
Edesy uses JSON with snake_case names.
| Twilio parameter | Edesy field |
|---|---|
To, From |
to, from |
Url |
url (must return VoiceML) |
Method |
method |
Twiml |
voiceml (a VoiceML JSON string), or text for a simple message |
ApplicationSid |
application_id (application_sid also accepted) |
StatusCallback |
status_callback |
StatusCallbackMethod |
status_method |
Timeout |
timeout (5 to 600) |
| (display name) | caller_name, up to 50 characters |
MachineDetection |
machine_detection (accepted; detection with a stored verdict runs on campaigns today) |
| custom metadata | custom_parameters (echoed back on webhooks) |
Fields Edesy does not recognise are not errors: they are listed in meta.ignored_fields, which makes leftover Twilio parameters easy to spot.
Status callbacks#
The payload keeps Twilio's PascalCase names: CallSid, AccountSid, From, To, CallStatus, Direction, Duration. CallStatus uses the same values (initiated, ringing, in-progress, completed, busy, no-answer, failed, canceled).
Differences to plan for:
- With POST, the body is JSON, not form-encoded. Parse JSON.
- Edesy adds
SipResponseCode,HangupReasonandCallOutcomeon the final event, so you can tell "phone switched off" from "invalid number". See Call Events and Outcomes. - Your
custom_parameterscome back underCustom. - The signature header is
X-Webhook-Signature(HMAC-SHA256 over timestamp and body), notX-Twilio-Signature. See Status Callbacks. - Call SIDs look like
CA_plus a UUID.
VoiceML instead of TwiML#
This is the main code change. Edesy does not parse TwiML; your answer URL must return VoiceML JSON with Content-Type: application/json.
TwiML:
<Response>
<Say language="en-IN">Thanks for calling Acme.</Say>
<Play>https://cdn.example.com/hold.mp3</Play>
<Hangup/>
</Response>
VoiceML:
{
"instructions": [
{ "verb": "say", "text": "Thanks for calling Acme.", "language": "en-IN" },
{ "verb": "play", "url": "https://cdn.example.com/hold.mp3" },
{ "verb": "hangup" }
]
}
| TwiML verb | VoiceML verb | Notes |
|---|---|---|
<Say> |
say |
text, language, voice |
<Play> |
play |
url, loop |
<Gather> |
gather |
Keypad digits. For speech, use Conversation Relay or Media Streams |
<Record> |
record |
max_length, play_beep, finish_on_key |
<Dial><Number> |
dial |
number, caller_id, timeout |
<Connect><Stream> / <Stream> |
stream, or better stream_url |
|
<Redirect> |
redirect |
|
<Enqueue> |
enqueue |
In the schema but not executed today; use the Edesy contact center for queues |
<Reject> |
reject |
SIP status (default 603) and reason |
<Hangup> |
hangup |
|
<Pause> |
pause |
length in seconds |
Multi-step flows that chain <Gather action> callbacks do not translate one-to-one: Edesy runs the document your answer URL returns, and action callbacks are not covered by these docs yet. Rebuild multi-step keypad menus in IVR Studio, or move conversational flows to Conversation Relay. Full reference: VoiceML Reference.
If you have many TwiML handlers, one option is a small adapter in your own code that maps your TwiML builder's output to the VoiceML JSON above.
Media Streams#
Twilio Media Streams sends base64 mu-law audio inside JSON messages. Edesy Media Streams sends raw L16 PCM (16-bit, mono, 8 or 16 kHz) as binary WebSocket frames after one JSON metadata frame, and takes killAudio / disconnect control messages. A stock Twilio serializer will not work; swap it for an Edesy one. See Media Streams, which includes a Pipecat serializer.
ConversationRelay#
The Conversation Relay protocol is compatible with Twilio ConversationRelay: the same setup, prompt, interrupt, dtmf messages in, and text, play, sendDigits, language, end out. A bot written for Twilio usually works after pointing an Edesy number at its URL. See Conversation Relay.
Numbers and billing#
- Numbers: buy Indian mobile, landline, toll-free and 92-series virtual numbers self-serve after KYC. You can also bring a Twilio Elastic SIP trunk to Edesy as a carrier; see SIP trunking.
- Billing: prepaid INR wallet topped up with Razorpay, GST invoices with your GSTIN, Rs 0.50 per minute with a 60-second pulse, unanswered calls free, no minimum commitment. Prices exclude 18% GST.
- SDKs: Edesy does not publish server SDK packages. Call the REST API with your HTTP client; the endpoints are small.
Migration checklist#
- Complete KYC and buy (or bring) your numbers.
- Create an API key, or copy your Account SID and Auth Token.
- Change the base URL to
https://voice-api.edesy.in/v1and send JSON bodies. - Convert answer-URL handlers from TwiML to VoiceML.
- Update status-callback handlers to parse JSON and read
HangupReason/CallOutcome. - Swap Media Streams serializers, if you use them.
- Route inbound numbers with PUT
/v1/phone-numbers/{id}/inbound. - Place test calls and read GET
/v1/calls/{CallSid}/eventsfor each path.
Questions about a larger migration: /contact or book a demo.