# Migrate from Twilio

> Move Twilio Programmable Voice code to the Edesy Voice API - endpoint mapping, Basic auth with Account SID and Auth Token, PascalCase status callbacks, and the one real difference - VoiceML JSON instead of TwiML.

Source: https://edesy.in/docs/voice-api/guides/migrate-from-twilio

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](https://edesy.in/docs/voice-api/calls/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](https://edesy.in/docs/voice-api/phone-numbers/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

```bash
# 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](https://edesy.in/docs/voice-api/getting-started/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`, `HangupReason` and `CallOutcome` on the final event, so you can tell "phone switched off" from "invalid number". See [Call Events and Outcomes](https://edesy.in/docs/voice-api/calls/call-events-and-outcomes).
- Your `custom_parameters` come back under `Custom`.
- The signature header is `X-Webhook-Signature` (HMAC-SHA256 over timestamp and body), not `X-Twilio-Signature`. See [Status Callbacks](https://edesy.in/docs/voice-api/calls/status-callbacks#verify-the-signature).
- 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:

```xml
<Response>
  <Say language="en-IN">Thanks for calling Acme.</Say>
  <Play>https://cdn.example.com/hold.mp3</Play>
  <Hangup/>
</Response>
```

VoiceML:

```json
{
  "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](https://ivr.edesy.in), or move conversational flows to Conversation Relay. Full reference: [VoiceML Reference](https://edesy.in/docs/voice-api/calls/voiceml).

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](https://edesy.in/docs/voice-api/streaming/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](https://edesy.in/docs/voice-api/streaming/conversation-relay).

## Numbers and billing

- **Numbers:** buy Indian mobile, landline, toll-free and 92-series [virtual numbers](https://edesy.in/virtual-phone-number) self-serve after KYC. You can also bring a Twilio Elastic SIP trunk to Edesy as a carrier; see [SIP trunking](https://edesy.in/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

1. Complete KYC and buy (or bring) your numbers.
2. Create an API key, or copy your Account SID and Auth Token.
3. Change the base URL to `https://voice-api.edesy.in/v1` and send JSON bodies.
4. Convert answer-URL handlers from TwiML to VoiceML.
5. Update status-callback handlers to parse JSON and read `HangupReason` / `CallOutcome`.
6. Swap Media Streams serializers, if you use them.
7. Route inbound numbers with `PUT /v1/phone-numbers/{id}/inbound`.
8. Place test calls and read `GET /v1/calls/{CallSid}/events` for each path.

Questions about a larger migration: [/contact](https://edesy.in/contact) or [book a demo](https://edesy.in/demo).
