> ## 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 Sinch to Orbit: SMS, Conversation API, and Verification

> Migrate your Sinch integration to Orbit step by step, mapping service plans, batch sends, delivery webhooks, Conversation API channels, and Sinch Verification to their Orbit equivalents.

# Migration from Sinch to Orbit

This guide walks you through migrating your SMS, multipurpose Conversation API, and Verification integration from Sinch to Orbit. Sinch's account shape — a `service_plan_id`-scoped API token, a batch-only send surface, and (for omnichannel) the Conversation API "app" wrapping every channel — differs from Orbit's model, and each of those concepts has a direct equivalent. The migration is incremental: every step below works alongside your live Sinch traffic until you cut over.

## Concept Mapping

| Sinch Concept                                    | Orbit Equivalent                                                                                                 | Notes                                                                                                                                                                                                          |
| ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `service_plan_id` + API token                    | API key (`dv_live_sk_xxxx`) in the `X-API-Key` header                                                            | One key authenticates every endpoint — no per-product credentials                                                                                                                                              |
| SMS batch send (`Batches` resource)              | `POST /api/v1/messages` (one recipient) or `POST /api/v1/messages/batch` (`recipients[]`, up to 10,000 per call) | Sinch's only SMS shape is batch; in Orbit a batch exists for scale and single send is the common path                                                                                                          |
| Conversation API app (SMS/WhatsApp/RCS superapp) | Messaging Service + channel                                                                                      | A service id (`msvc_…`) bundles its sender pool, opt-out list, inbound webhook, and throughput cap — pass it as `messaging_service_id`; the channel is picked explicitly per send instead of living in one app |
| Delivery-report webhooks (DLR envelope)          | Webhook events (`message.delivered`, `message.failed`, inbound `message.received`)                               | Same event-driven pattern, JSON envelope with an `X-Orbit-Signature` HMAC                                                                                                                                      |
| Sinch Numbers                                    | Orbit Numbers                                                                                                    | Same capabilities; LOA-based porting supported also from Sinch                                                                                                                                                 |
| Sinch Verification                               | Orbit Verify                                                                                                     | Send + check calls plus `verification.*` webhook events, same two-call OTP model                                                                                                                               |

***

## Prefer not to do it by hand?

The import tooling covers the configuration layer — numbers, messaging services, templates, and consent/contact data — before you touch any code:

* **Dashboard wizard** — **Settings → Import** walks you through connecting a read-only credential, a dry-run preview, and a one-click commit with rollback.
* **CLI** — `npm install -g @devotel/cli`, `devotel auth login`, then preview with `devotel migrate` and commit with `--run`.

The wizard imports your *configuration* only — it never moves live traffic, so the Sinch integration keeps running until you cut over. The rest of this guide covers the manual, code-level migration for teams who want full control over each step.

***

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

Replace every `Authorization: Bearer <service_plan_id token>` header with `X-API-Key: dv_live_sk_xxxx` — one header, one key, for SMS, Verify, Numbers, and webhooks alike.

***

## Step 2: Port Your Numbers

Sinch supports LOA-based port-out, so you can move existing numbers to Orbit. The process takes 7–14 business days.

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.

```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 request — same call shape as a Twilio port, with the Sinch
#    account details you used to hold the number.
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", "+14155551390"],
    "currentCarrier": "Sinch",
    "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.

If the recipient is a US local number you'll reuse for outbound (including 10DLC traffic), port it before you point sends at Orbit, then finish the 10DLC registration in the dashboard — the [10DLC walkthrough](/guides/10dlc-wizard) covers the ordering.

***

## Step 3: Swap the SMS Send Path

Sinch's SMS surface is the `Batches` resource — batch-shaped even for a single recipient — so "one endpoint to all your to\[] values" has to be read as either a single send or a batch.

### Sinch (Before)

```javascript theme={null}
const response = await fetch(
  'https://sms.sinch.com/service_plan_id/sp_xxxxxxxx:batches',
  {
    method: 'POST',
    headers: {
      Authorization: 'Bearer sp_xxxxxxxx',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      from: 'mg_sendernumber',
      to: ['+14155552671'],
      body: 'Hello from Sinch!',
      delivery_report: 'full',
    }),
  }
)
```

### Orbit (After)

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

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

// Single recipient
await orbit.messages.send({
  channel: 'sms',
  to: '+14155552671',
  body: 'Hello from Orbit!',
})

// Batch — pass a recipients[] array with optional per-row variables
await fetch('https://api.orbit.devotel.io/api/v1/messages/batch', {
  method: 'POST',
  headers: {
    'X-API-Key': 'dv_live_sk_xxxx',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    channel: 'sms',
    recipients: [
      { to: '+14155552671', body: 'Hello {{ first_name }}!' },
      { to: '+14155552890' },
    ],
    // Or carry the body once for the whole batch:
    // body: 'Hello {{ first_name }}!',
  }),
})
```

### Field-by-field mapping

| Sinch send field                  | Orbit send field                                   | Notes                                                                                                                                            |
| --------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `from` (bare sender / `mg_*`)     | `messaging_service_id` (`msvc_…`) or a bare `from` | A service id pulls the sender pool, opt-out list, and inbound webhook in one shot; map the Sinch sender id via the service's `external_id` field |
| `to[]`                            | `to` (single) or `recipients[].to` (batch)         | `/messages/batch` accepts up to 10,000 recipients per call and returns a per-recipient outcome map                                               |
| `body`                            | `body` (shared) or `recipients[].body` (per-row)   | Per-row values take priority; a shared `body` fills every row without one                                                                        |
| `delivery_report`                 | Webhook subscription                               | See step 4 — you subscribe to `message.delivered` / `message.failed` instead of requesting a per-send flag                                       |
| `parameters` / template variables | `recipients[].variables` (per-row Liquid)          | `{{ first_name \| capitalize \| default: "friend" }}` per recipient in one request                                                               |
| `campaign`                        | `metadata` (per-row on batch)                      | Optional; useful alongside analytics joins                                                                                                       |

If your Sinch send was a single-recipient call, leave it single in Orbit. If it genuinely fanned out over `to[]`, move it to `/batch`; the per-recipient map mirrors what Sinch's per-batch DLR would have told you.

***

## Step 4: Migrate Delivery-Report Webhooks

Sinch pushes DLRs as HTTP callbacks into an envelope you registered on the batch. Orbit subscribers see the same signal as signed webhook events with a stable event id — you dedupe on it instead of building idempotency out of per-message state.

### Envelope mapping

**Sinch DLR callback (before)**

```json theme={null}
{
  "correlation_id": "cb_1",
  "status": "Delivered",
  "type": "delivery_status"
}
```

**Orbit webhook event (after)**

```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"
  }
}
```

### Handler migration

1. Subscribe once — `POST /api/v1/webhooks` with `events: ["message.delivered", "message.failed", "message.received"]` — instead of embedding `delivery_report: "full"` on every send.
2. Read the **raw** body before parsing, and verify against `X-Orbit-Signature` (HMAC-SHA256 over `timestamp.body`). `X-Devotel-Signature` still exists for backward compatibility only.
3. Dedupe on the envelope `id` — Orbit retries at-least-once for about 4–5 hours, so the id is the durable key for your `seen_events` table.
4. Ack fast (`2xx` in milliseconds) and hand the work to a queue.

Full walkthrough with verified Node.js and Python receivers in the [webhook consumer guide](/guides/webhook-consumer).

| Sinch webhook concept                           | Orbit equivalent                                |
| ----------------------------------------------- | ----------------------------------------------- |
| Callback URL registered per-batch               | One subscription URL on `POST /api/v1/webhooks` |
| `correlation_id` (per batch)                    | Durable event envelope `id`                     |
| HMAC-less signature / API-key-style bearer auth | `X-Orbit-Signature` (HMAC-SHA256) header        |
| Per-message `status` map                        | `message.delivered` / `message.failed` events   |

***

## Step 5: Map the Conversation API

Sinch's Conversation API wraps multiple channels inside one "app" that owns the sender and the inbound webhook. In Orbit the equivalent construct is a Messaging Service scoped to a channel, and sends name the channel explicitly. If your Sinch app handled **WhatsApp** — the most common Conversation-API case — the split looks exactly like the WhatsApp guides.

| Conversation API concept                | Orbit equivalent                                                                                                           |
| --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| App (SMS + WhatsApp + RCS in one scope) | One Messaging Service per channel (`msvc_…`), or a service + WhatsApp channel selection                                    |
| Inbound webhook registered on the app   | Inbound webhook on the service (`Inbound URL`) — see [Messaging services console](/guides/messaging-services-console)      |
| Template approval flow for WhatsApp     | [WhatsApp templates](/guides/whatsapp/templates-create-approve) — the same approval cycle surfaces through the WABA guides |
| Channel selection implicit under app    | `channel` field on send (`sms`, `whatsapp`, `rcs`, …)                                                                      |

If your Sinch app mixed SMS and WhatsApp traffic, split it into a SMS Messaging Service plus the WhatsApp WABA path — you'll get a per-channel throughput cap and a cleaner opt-out list instead of one shared envelope.

***

## Step 6: Migrate Verification

Sinch Verification is the same send/check OTP model as Orbit Verify — two backend calls, plus an optional webhook for asynchronous completion.

### Sinch (Before)

```javascript theme={null}
await sinch.verifyments.start({
  identity: { type: 'number', endpoint: '+14155552671' },
  method: 'sms',
})

const result = await sinch.verifyments.report({
  identity: { type: 'number', endpoint: '+14155552671' },
  method: 'sms',
  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',
})
```

If you don't want a client library, plain HTTPS works end-to-end — `POST /api/v1/verify/send` then `POST /api/v1/verify/check` — see [Verify without the SDK](/guides/verify-no-sdk).

***

## Migration Checklist

* [ ] Create Orbit account and generate an API key
* [ ] Port numbers (or purchase new ones) with an LoA
* [ ] Install the Orbit SDK (`npm install @devotel-orbit/node`) or use plain HTTPS
* [ ] Swap single sends to `POST /api/v1/messages`, batches to `POST /api/v1/messages/batch`
* [ ] Rewire per-batch `delivery_report` flags to one webhook subscription
* [ ] Update webhook handler to verify `X-Orbit-Signature` and dedupe on event `id`
* [ ] Split any Conversation API app into a per-channel Messaging Service (+ WhatsApp WABA if applicable)
* [ ] Migrate Verification to `/verify/send` + `/verify/check`
* [ ] Update monitoring and alerting for the new event names
* [ ] Run parallel sends against both providers
* [ ] Decommission the Sinch `service_plan_id` credential only after cutover
* [ ] Cancel the Sinch account once cutover is stable

***

## Parallel Running Strategy

We recommend running Sinch and Orbit in parallel during migration:

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

<Tip>
  Only decommission the `service_plan_id` credential after cutover is stable — a leftover token that still verifies forces you to treat signature failures as auth bugs.
</Tip>

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