> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Migration from Twilio

> Step-by-step guide to migrate your Twilio integration to Orbit

# Migration from Twilio to Orbit

This guide walks you through migrating your SMS, voice, and messaging integration from Twilio to Orbit. Most Twilio concepts map directly to Orbit equivalents, and the migration can be completed incrementally.

## Concept Mapping

| Twilio Concept           | Orbit Equivalent            | Notes                                                                                                                                                   |
| ------------------------ | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Account SID + Auth Token | API Key (`dv_live_sk_xxxx`) | Single key, simpler auth                                                                                                                                |
| Phone Numbers            | Numbers API                 | Same capabilities, lower cost                                                                                                                           |
| Messaging Service        | Messaging Services          | Bundles senders + opt-out + inbound webhook; pass its id as `messaging_service_id` when you send. Map your old `MessagingServiceSid` via `external_id`. |
| TwiML                    | IVR API + Agents            | Declarative IVR or AI agents                                                                                                                            |
| Studio Flows             | Flows (visual builder)      | Drag-and-drop flow builder                                                                                                                              |
| Programmable Voice       | Voice API                   | Calls, SIP, conferences                                                                                                                                 |
| Verify                   | Verify API                  | OTP/2FA                                                                                                                                                 |
| Conversations            | Agent conversations         | AI-native conversations                                                                                                                                 |
| Webhooks                 | Webhooks                    | Same pattern, different headers                                                                                                                         |
| Status Callbacks         | Webhook events              | `message.delivered`, `call.completed`                                                                                                                   |

***

## Step 1: Create Your Orbit Account

1. Sign up at [orbit.devotel.io/signup](https://orbit.devotel.io/signup)
2. Generate an API key at **Settings > API Keys**
3. Note your key prefix: `dv_live_sk_xxxx`

***

## Step 2: Port Your Numbers

You can port existing Twilio numbers to Orbit. The process takes 7–14 business days.

**Via API:**

A port request must carry a Letter of Authorization (LoA). Orbit only accepts an
LoA URL it issued itself, so you upload the signed PDF to Orbit first and submit the
returned URL — a link to your own bucket (for example
`https://storage.googleapis.com/your-loa-bucket/loa.pdf`) is rejected with
`LOA_URL_INVALID_ORIGIN`.

```bash theme={null}
# 1. Upload the signed LoA PDF. The response `data.url` is a short-lived signed
#    URL (1-hour default). Pass `ttl_ms` to keep it valid long enough for the
#    carrier to fetch it during submission — here, 24 hours.
curl -X POST "https://api.orbit.devotel.io/api/v1/files/upload?ttl_ms=86400000" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -F "file=@./loa.pdf;type=application/pdf"
# => { "data": { "id": "file_...", "url": "https://storage.googleapis.com/...", ... } }
```

```bash theme={null}
# 2. Submit the port request, passing the uploaded `data.url` as loaFileUrl.
curl -X POST https://api.orbit.devotel.io/api/v1/numbers/porting \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "numbers": ["+14155551234", "+14155551235"],
    "currentCarrier": "Twilio",
    "country": "US",
    "authorizedSigner": "Your Name",
    "accountNumber": "123456789",
    "accountPin": "1234",
    "loaFileUrl": "https://storage.googleapis.com/.../loa.pdf?X-Goog-Signature=..."
  }'
```

**Alternative:** Purchase new numbers and update your systems gradually.

***

## Step 3: Update SMS Integration

### Twilio (Before)

```javascript theme={null}
const twilio = require('twilio')
const client = twilio('ACXXXXXXX', 'auth_token')

await client.messages.create({
  to: '+14155552671',
  from: '+18005551234',
  body: 'Hello from Twilio!',
  statusCallback: 'https://yourapp.com/webhooks/sms',
})
```

### Orbit (After)

```javascript theme={null}
import { Devotel } from '@devotel/orbit-sdk'

const orbit = new Devotel({ apiKey: 'dv_live_sk_xxxx' })

await orbit.messages.send({
  channel: 'sms',
  to: '+14155552671',
  body: 'Hello from Orbit!',
  status_callback: 'https://yourapp.com/webhooks/sms',
})
```

### Key Differences

| Feature           | Twilio                     | Orbit                                                      |
| ----------------- | -------------------------- | ---------------------------------------------------------- |
| Auth              | Account SID + Token        | Single API key in `X-API-Key`                              |
| Send endpoint     | `client.messages.create()` | `orbit.messages.send()`                                    |
| Channel selection | Implicit (number type)     | Explicit `channel` field                                   |
| From number       | Required `from` field      | Auto-selected or specified                                 |
| Status webhook    | `statusCallback`           | `status_callback` (REST: `webhook_url`) or global webhooks |

***

## Step 4: Update Webhook Handlers

### Webhook Header Changes

| Twilio                           | Orbit                             |
| -------------------------------- | --------------------------------- |
| `X-Twilio-Signature` (HMAC-SHA1) | `X-Orbit-Signature` (HMAC-SHA256) |

Verify against `X-Orbit-Signature` — it is the canonical header Orbit sends on every webhook delivery. `X-Devotel-Signature` is still emitted for backward compatibility only; treat it as legacy and do not build new verifiers against it.

### Payload Format Changes

**Twilio (form-encoded):**

```
MessageSid=SM123&From=%2B14155551234&To=%2B14155552671&Body=Hello&MessageStatus=delivered
```

**Orbit (JSON):**

```json theme={null}
{
  "id": "evt_msg_001",
  "type": "message.delivered",
  "created_at": "2026-03-08T12:00:00Z",
  "data": {
    "message_id": "msg_abc123",
    "channel": "sms",
    "from": "+14155551234",
    "to": "+14155552671",
    "status": "delivered"
  }
}
```

### Update Signature Verification

```javascript theme={null}
// Twilio (before)
const valid = twilio.validateRequest(authToken, signature, url, params)

// Orbit (after)
import { verifyWebhookSignature } from '@devotel/orbit-sdk'
const valid = verifyWebhookSignature(rawBody, signature, 'whsec_your_secret')
```

***

## Step 5: Migrate Voice (if applicable)

### TwiML to Orbit IVR

**Twilio TwiML:**

```xml theme={null}
<Response>
  <Gather input="dtmf" numDigits="1" action="/handle-key">
    <Say>Press 1 for sales, 2 for support.</Say>
  </Gather>
</Response>
```

**Orbit IVR API:**

Orbit models IVRs as **IVR flows** — a `{ nodes, edges }` graph (the same shape the
visual [IVR builder](https://orbit.devotel.io/voice/ivr-builder) saves). Create a draft
flow with `POST /api/v1/voice/ivr-flows`, then publish it so inbound callers see it:

```bash theme={null}
# 1. Create the draft flow
curl -X POST https://api.orbit.devotel.io/api/v1/voice/ivr-flows \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Main Menu",
    "active": true,
    "definition": {
      "nodes": [
        { "id": "start", "type": "ivrStart", "data": {} },
        {
          "id": "menu",
          "type": "menu",
          "data": {
            "prompt": "Press 1 for sales, 2 for support.",
            "menuOptions": [
              { "key": "1", "label": "Sales" },
              { "key": "2", "label": "Support" }
            ]
          }
        },
        { "id": "sales", "type": "transfer", "data": { "transferTo": "+14155550101" } },
        { "id": "support", "type": "transfer", "data": { "transferTo": "+14155550102" } }
      ],
      "edges": [
        { "id": "e1", "source": "start", "target": "menu" },
        { "id": "e2", "source": "menu", "target": "sales" },
        { "id": "e3", "source": "menu", "target": "support" }
      ]
    }
  }'
```

The graph is validated on save: it must have exactly one entry node (`ivrStart`), at
least one terminal node (`transfer`, `hangup`, `voicemail`, `ringGroup`, …), no
unreachable nodes, and no loops without an exit. DTMF `menuOptions[].key` values must be
`0`–`9`, `*`, or `#`. Per invariant #45, `transfer` targets must be phone numbers or
on-net extensions — raw `sip:` URIs are rejected (outbound voice exits only via the
Devotel softswitch).

```bash theme={null}
# 2. Publish the draft to the live runtime (returns the new version number)
curl -X POST https://api.orbit.devotel.io/api/v1/voice/ivr-flows/{flowId}/publish \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

<Tip>
  Prefer not to hand-author the graph? Build the flow visually at **Voice > IVR Builder**
  and publish from the canvas — the API above is the same surface the builder calls.
</Tip>

### Upgrade to AI Agents

Instead of static IVR trees, consider deploying an AI voice agent:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/agents \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Voice Support Agent",
    "type": "voice",
    "model": "claude-sonnet-4-6",
    "system_prompt": "You are a helpful phone support agent for Acme Corp.",
    "channels": ["voice"]
  }'
```

***

## Step 6: Migrate Verify (OTP)

### Twilio (Before)

```javascript theme={null}
await client.verify.v2.services('VA123').verifications.create({
  to: '+14155552671',
  channel: 'sms',
})

const check = await client.verify.v2.services('VA123').verificationChecks.create({
  to: '+14155552671',
  code: '123456',
})
```

### Orbit (After)

```javascript theme={null}
const verification = await orbit.verify.send('+14155552671', 'sms')

const result = await orbit.verify.check({
  verification_id: verification.data.verification_id,
  code: '123456',
})
```

***

## Migration Checklist

* [ ] Create Orbit account and generate API keys
* [ ] Port numbers or purchase new ones
* [ ] Install Orbit SDK (`npm install @devotel/orbit-sdk`)
* [ ] Update message sending code
* [ ] Update webhook endpoint handlers
* [ ] Update webhook signature verification
* [ ] Migrate voice/IVR flows (if applicable)
* [ ] Migrate Verify/OTP (if applicable)
* [ ] Update monitoring and alerting
* [ ] Run parallel testing (send via both Twilio and Orbit)
* [ ] Decommission Twilio integration
* [ ] Cancel Twilio account

***

## Parallel Running Strategy

We recommend running Twilio and Orbit in parallel during migration:

1. **Phase 1 (Week 1–2):** Send 10% of traffic through Orbit, 90% through Twilio
2. **Phase 2 (Week 3–4):** Split 50/50 and compare delivery rates
3. **Phase 3 (Week 5):** Route 100% through Orbit, keep Twilio as fallback
4. **Phase 4 (Week 6+):** Decommission Twilio

<Tip>
  Need help with your migration? Our solutions team offers free migration support for customers moving from Twilio. Contact [migrate@devotel.io](mailto:migrate@devotel.io).
</Tip>
