# How to Migrate Twilio Calls API Code to Edesy (2026)

> A step-by-step guide to moving Twilio Programmable Voice code to Edesy: swap the base URL and keys, map request fields, convert TwiML to VoiceML JSON, and update status callback handling.

Source: https://edesy.in/blog/migrate-twilio-calls-api-to-edesy-2026

**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](https://voice-app.edesy.in/?utm_source=edesy.in&utm_medium=blog&utm_campaign=migrate-twilio-calls-api-to-edesy-2026). Complete KYC, buy a number and create an API key.

If you are still deciding whether to move, read [the honest Twilio comparison](https://edesy.in/blog/twilio-alternative-for-india-developers-2026)
first. This post assumes you have decided to try it.

## Before you start

1. Sign up on [voice-app.edesy.in](https://voice-app.edesy.in/?utm_source=edesy.in&utm_medium=blog&utm_campaign=migrate-twilio-calls-api-to-edesy-2026)
   and complete KYC (PAN and Aadhaar; review takes about 24 hours).
2. Top up the INR wallet with Razorpay.
3. Buy an Indian number (mobile, 92-series, landline or toll-free) to use as the caller ID.
4. 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`:

```js
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):

```xml
<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):

```json
{
  "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:

1. **Parse JSON.** POST callbacks arrive as a JSON body (GET callbacks put the same fields in the query string).
2. **Verify the signature.** Edesy signs each delivery with HMAC-SHA256 in `X-Webhook-Signature` (`v1=<hex>`)
   and `X-Webhook-Timestamp`. The signed message is `<timestamp>.<raw body>`.
3. **Use the extra fields.** `SipResponseCode`, `HangupReason` and `CallOutcome` tell you why a call ended.
   See [call status callbacks explained](https://edesy.in/blog/call-status-callbacks-explained-2026) 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](https://edesy.in/voice-api/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](https://edesy.in/blog/websocket-audio-streaming-voice-bot-2026), or the
  [Pipecat guide](https://edesy.in/blog/connect-pipecat-bot-to-indian-phone-number-2026) if you use Pipecat.

## Step 6: Test and cut over

1. Run each flow against a real phone from the portal's "Make a call" screen.
2. Check the per-call event timeline: `GET /v1/calls/:sid/events`.
3. Check the debugger and webhook delivery log; replay any failed delivery.
4. 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](https://edesy.in/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](https://edesy.in/voice-api/pricing).

The [Voice API docs](https://edesy.in/docs/voice-api) have the full field reference and the VoiceML verb reference. For an
overview, see the [Voice API hub](https://edesy.in/voice-api) and [Twilio alternative for India](https://edesy.in/twilio-alternative-india).

## Frequently Asked Questions

### How long does a Twilio to Edesy migration take?

It depends on how much TwiML you have. Swapping the base URL, credentials and request fields is quick. Converting answer handlers from TwiML to VoiceML JSON is the main work, and it scales with the number of call flows you have.

### Can I keep my Twilio Account SID style auth?

Yes. Edesy accepts Basic auth with your Edesy Account SID and Auth Token, and the /v1/Accounts/:sid/Calls path. You can also use a Bearer API key instead.

### Do I need to change my status callback handler?

Only a little. Edesy sends Twilio-style PascalCase fields such as CallSid, CallStatus and Duration. POST callbacks arrive as a JSON body rather than form-encoded, and there are extra fields (SipResponseCode, HangupReason, CallOutcome) you can use for retry logic.

### Will my ConversationRelay bot work on Edesy?

Yes. Edesy's Conversation Relay uses Twilio's ConversationRelay protocol, so the bot code works unchanged. You point an Edesy number at the bot's WebSocket URL.

### Can I port my existing Indian numbers?

You can buy new Indian numbers on Edesy after KYC, or use bring-your-own-carrier recipes (Twilio Elastic SIP, Telnyx, Vonage, Plivo) to keep traffic on an existing carrier while you test. Talk to us about specific number moves.

### Is there a Twilio-style Node or Python SDK?

No. Edesy has no server SDK packages today. Replace twilio.calls.create() with a plain HTTPS request to POST /v1/calls using fetch, axios or requests.
