> ## 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.

# Migrate from Vonage to Orbit: SMS, voice, and webhooks

> Migrate your Vonage Messages and Voice API integration to Orbit step by step, mapping Vonage Applications, numbers, JWT auth, signature verification, and NCCO call control to their Orbit equivalents.

# Migration from Vonage to Orbit

This guide walks you through migrating your SMS, voice, and webhook integration from Vonage (formerly Nexmo) to Orbit. The migration moves fastest if you translate your Vonage-shaped concepts into Orbit concepts first — most have a direct equivalent. Follow the [Migrate from Twilio](/guides/migration-from-twilio) guide's sibling structure if you run multiple vendor comparisons.

## Concept Mapping

| Vonage Concept                                      | Orbit Equivalent                                         | Notes                                                                                                           |
| --------------------------------------------------- | -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| API key + secret, or Application `private.key` JWT  | API Key (`dv_live_sk_xxxx`)                              | One key in the `X-API-Key` header — no JWT generation or keypair rotation                                       |
| Vonage Applications (voice + webhook URLs)          | [Messaging Services](/guides/messaging-services-console) | Bundles senders, opt-out rules, and inbound webhook config; pass its id as `messaging_service_id` when you send |
| Vonage Numbers                                      | [Numbers](/guides/buy-numbers) API                       | Buy, port, and assign numbers per service                                                                       |
| Vonage Conversations API                            | Agent conversations                                      | AI-native conversations (see [two-way SMS conversations](/guides/sms-two-way-conversation))                     |
| `signature` JWT per webhook (or webhook JWT toggle) | `X-Orbit-Signature` HMAC-SHA256 header                   | Verify per the [webhook consumer](/guides/webhook-consumer) guide; no JWT library needed                        |
| NCCO (JSON call-control objects)                    | IVR flows API + voice agents                             | `{ nodes, edges }` graphs; the visual builder saves the same shape                                              |
| Vonage Verify                                       | [Verify](/guides/verify-in-30-min) API                   | OTP/2FA with fallback chains                                                                                    |
| Delivery receipts (`dlr`)                           | `message.delivered` / `message.failed` events            | Same status funnel, different event names                                                                       |
| Inbound SMS webhook                                 | `message.inbound` event                                  | Registered on a webhook endpoint, not per-number                                                                |

***

## Prefer not to do it by hand?

Honest answer up front: the assisted import wizard (dashboard: **Settings → Import**; CLI: `devotel migrate <source>`) currently ships connectors for **Twilio, Klaviyo, MessageBird, and Telnyx** — Vonage is not on the list yet, so this guide is a manual-level path with no one-click wizard. Every step below is copy-paste-ready and the two **long-horizon pieces** (porting numbers and retooling webhook handlers) are the only parts that touch your account before cut-over; the rest is code on your side.

If you have a large estate and want the sequence scripted end-to-end, the same import pipeline carries an `external_id` label per entity even for manual flows, so your backfill reports stay vendor-consistent. See [Platform migration jobs](/guides/platform-migration-jobs) for the job runner.

***

## 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`

Unlike Vonage — where an Application-scoped JWT signs each request — Orbit authenticates with a flat API key sent in the `X-API-Key` header. There is no keypair, no `private.key` file, and no JWT expiry to babysit.

***

## Step 2: Port Your Numbers

Port existing Vonage numbers to Orbit following the [Porting numbers end-to-end](/guides/port-numbers) flow. Expect 7–14 business days; Vonage's Letter of Authorization (LoA) flow needs the **Customer Service Record (CSR)** from your Vonage account page, not guessed values — rejections like `ADDRESS_MISMATCH` and `NAME_MISMATCH` almost always trace back to a copied-instead-of-guessed field.

Vonage quirks to note on the CSR pull:

* If your Vonage account holds numbers across sub-accounts, pull the CSR for the **master** account; the legal entity name must match the one Vonage bills.
* SMS-enabled numbers: declare it on the LoA so the carrier ports voice and messaging together; a response of `splittable: true` on the portability check warns you the carrier may move voice only.

A port request must carry an LoA URL issued by Orbit, so upload the signed PDF to Orbit first and submit the returned URL — a link to your own bucket 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": "Vonage",
    "country": "US",
    "authorizedSigner": "Your Name",
    "accountNumber": "123456789",
    "accountPin": "1234",
    "loaFileUrl": "https://storage.googleapis.com/.../loa.pdf?X-Goog-Signature=..."
  }'
```

**Alternative:** purchase new numbers on Orbit and update your systems gradually — the full checklist is in the porting guide linked above.

***

## Step 3: Rewrite SMS Sends

### Vonage (before)

```javascript theme={null}
const { Vonage } = require('@vonage/server-sdk')

const vonage = new Vonage({
  apiKey: 'vongexxx',
  apiSecret: 'secret',
})

await vonage.messages.send({
  to: '+14155552671',
  from: '+18005551234',
  message_type: 'text',
  text: 'Hello from Vonage!',
})
```

### Orbit (after)

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

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         | Vonage                                                         | Orbit                                                                       |
| --------------- | -------------------------------------------------------------- | --------------------------------------------------------------------------- |
| Auth            | API key + secret (or per-request JWT)                          | Single API key in `X-API-Key`                                               |
| Send endpoint   | `POST /v1/messages` (or SMS API `/rest/sms`)                   | `POST /api/v1/messages`                                                     |
| Payload format  | `message_type` + `text` (+ optional per-message `webhook_url`) | `channel` + `body` (`status_callback` optional)                             |
| From number     | Required `from` field                                          | Auto-selected or specified                                                  |
| Delivery status | `dlr` webhook configured per Application                       | `message.delivered` / `message.failed` events or `status_callback` per send |

Vonage's Messages API accepted channel alternatives (`sms`, `mms`, `whatsapp`, …) in `message_type`; Orbit makes the channel an explicit `channel` field, which reads the same way in code.

***

## Step 4: Rewrite Inbound and Status Webhook Handlers

### Webhook Header Changes

| Vonage                                                                                                     | Orbit                                  |
| ---------------------------------------------------------------------------------------------------------- | -------------------------------------- |
| `signature` field inside the payload (JWT trickle, validated with your public key) or Authorization header | `X-Orbit-Signature` HMAC-SHA256 header |

Verify against `X-Orbit-Signature` — it is the canonical header Orbit sends on every delivery. `X-Devotel-Signature` is still emitted for backward compatibility only; treat it as legacy and do not build new verifiers against it. The [webhook consumer](/guides/webhook-consumer) guide has the full receiver recipe (raw-body capture, timestamp window, rotation grace header `X-Orbit-Signature-Next`).

### Payload Format Changes

Vonage sends webhook payloads as JSON at application level, but inbound SMS (from the legacy SMS API) arrives as query/form-encoded pairs. Orbit delivers one canonical JSON envelope for every event:

**Vonage SMS API inbound (form-encoded):**

```
msisdn=14155551234&to=18005551234&text=Hello&messageId=0B00000123
```

**Orbit inbound (JSON):**

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

### Event-Name Mapping

Map each Vonage webhook subscription to its Orbit event type. Subscribe your endpoint to the Orbit names in the table; the full catalog is at [Webhook events](/guides/webhook-event-catalog).

| Vonage webhook                                                                         | Orbit event type                                                          |
| -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| SMS API inbound (`/sms` webhook)                                                       | `message.inbound`                                                         |
| Messages API inbound (`message.inbound` under Messages API)                            | `message.inbound`                                                         |
| Delivery receipt (`dlr` / `message.status` with `status: delivered\|failed\|rejected`) | `message.delivered` / `message.failed`                                    |
| Voice `event_url` (answer/completed)                                                   | `call.completed`                                                          |
| RTC/Conversation events (member joined, message sent)                                  | Agent conversation events (optional — most teams drop these on migration) |

### Signature Verification (Vonage JWT trickle → Orbit HMAC)

```javascript theme={null}
// Vonage (before) — JWT signature validation per webhook
import jwt from 'jsonwebtoken'
const valid = jwt.verify(payload.signature, publicKey, { algorithms: ['RS256'] })

// Orbit (after) — shared-secret HMAC over the raw body
import { verifyWebhookSignature } from '@devotel-orbit/node'
const valid = verifyWebhookSignature(rawBody, signature, 'whsec_your_secret')
```

Orbit's scheme is plain HMAC-SHA256 over the exact raw request body with a shared `whsec_` secret you copy once when you register the endpoint. Most teams remove a JWT library from the receiver's dependency list at this step.

***

## Step 5: Migrate Voice (if applicable)

### NCCO to Orbit IVR

Vonage steers a call with NCCO — a JSON array of actions (`talk`, `input`, `connect`, `stream`, …) returned from your `answer_url`. In Orbit, the equivalent is an **IVR flow**: a `{ nodes, edges }` graph (the same shape the visual [IVR builder](https://orbit.devotel.io/voice/ivr-builder) saves), created with `POST /api/v1/voice/ivr-flows` and published so inbound callers see it.

**Vonage NCCO (from your `answer_url`):**

```json theme={null}
[
  {
    "action": "talk",
    "text": "Press 1 for sales, 2 for support."
  },
  {
    "action": "input",
    "type": ["dtmf"],
    "dtmf": { "maxDigits": 1 }
  }
]
```

**Orbit IVR API:**

```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: 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 `#`. Transfer targets must be phone numbers or on-net extensions — raw `sip:` URIs are rejected at save (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)

### Vonage (before)

```javascript theme={null}
// Vonage Verify API request/verify flow
const verification = await vonage.verify.start({
  number: '+14155552671',
  brand: 'Acme Corp',
})

const check = await vonage.verify.check(verification.request_id, '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',
})
```

Orbit's Verify API is send-then-check per request — no per-request brand config is needed because the brand name sits on your Verify profile, not per-call. Fallback chains (SMS → voice) live at the profile level too; see [Verify in 30 minutes](/guides/verify-in-30-min) for the profile setup and the wrong-code error shape.

***

## Migration Checklist

* [ ] Create Orbit account and generate API keys
* [ ] Pull the Vonage CSR (master account, exact entity name)
* [ ] Port numbers or purchase new ones
* [ ] Install Orbit SDK (`npm install @devotel-orbit/node`)
* [ ] Rewrite SMS send calls (Messages + SMS API callers)
* [ ] Rewrite inbound webhook handlers
* [ ] Rewrite delivery-receipt (dlr) handlers
* [ ] Swap JWT signature checks for HMAC verification
* [ ] Migrate NCCO → IVR flows (if applicable)
* [ ] Migrate Verify/OTP (if applicable)
* [ ] Update monitoring and alerting
* [ ] Run parallel testing (send via both Vonage and Orbit)
* [ ] Decommission Vonage integration
* [ ] Cancel Vonage account

***

## Parallel Running Strategy

We recommend running Vonage and Orbit in parallel during migration:

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

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