Skip to main content

API recipes: third-party migrators

The five per-provider migration guides — Twilio, Sinch, MessageBird, Vonage, Infobip — each document the same five migrator loops with provider-specific names. This page is the template: the loops, the expected envelopes, and the branch you take on each outcome, with Twilio as the worked example. Write a connector for another provider (Telnyx, Klaviyo, Bandwidth) by substituting the source-side identifiers; the Orbit side of every loop is provider-agnostic. Base URL is https://api.orbit.devotel.io/api/v1 throughout; authenticate with X-API-Key (test key dv_test_sk_… against the sandbox, live key for a cutover).
These are tenant-owned controls. Orbit supplies and enforces the migration surface; your organization names the source resource (a Messaging Service SID, a LoA file, a webhook URL), owns the source credentials, and makes the go-live call. These recipes are not legal advice.

0. Automate the whole import instead

Five loops below are the code-level migrator; two higher-level surfaces wrap them for operators. Reach for the wizard when the source account’s configuration (numbers, messaging services, templates, contacts) should land on Orbit without hand-mapping; reach for the loops when you need to drive the mapping from your own script or CI pipeline.
  • Assisted import wizard — Settings → Import → Twilio (or Sinch / MessageBird / Vonage / Infobip) connects a read-only credential, shows a dry-run preview of what lands where, and commits with rollback. Neither the wizard nor the loops touch your live traffic; the existing provider keeps running until you cut over. Full walkthrough: Assisted import wizard.
  • devotel migrate CLI — the same wizard scriptable from a terminal or pipeline. Install with npm install -g @devotel/cli, devotel auth login, then devotel migrate twilio --account-sid AC… --auth-token … (dry run first; add --run to commit). Flag reference: Devotel CLI guide.

1. Resolve a Twilio Messaging Service SID into messaging_service_id

Twilio groups senders under a Messaging Service; Orbit models the same bundle as a messaging service — senders plus opt-out rules plus an inbound webhook — and a send carries it on messaging_service_id. The mapping is a one-line metadata.external_id on the Orbit service you create, so the old MessagingServiceSid=SM… your systems log stays resolvable. List the existing services to find the one your script will tag (or to skip creation when a prior run already landed):
cURL
Create the Orbit service with the source id in metadata.external_id — the SID you used to pass to Twilio’s MessagingServiceSid parameter:
cURL
201 returns the created service — data.id (msg_svc_…) is the messaging_service_id every subsequent send carries:
201
Bind the numbers you ported (loop 2) into the service so inbound SMS resolves to the right webhook:
cURL
Send against it the same way your Twilio send carried MessagingServiceSid:
cURL
Re-resolve the SID to the Orbit id at cutover time — never bake msg_svc_… into your app; read GET /messaging/services once at startup, filter on metadata.external_id, and cache the mapping. Canonical pages: Messaging services console, Migration from Twilio.

2. Port the numbers with an Orbit-issued LoA URL

A port request must carry a Letter of Authorization. Orbit accepts only a LoA URL it issued itself, so you upload the signed PDF first (the upload returns a short-lived signed URL), then pass that URL as loaFileUrl. A link to your own bucket — https://storage.googleapis.com/your-loa-bucket/loa.pdf — is rejected 422 LOA_URL_INVALID_ORIGIN before the carrier sees it. Pre-check eligibility (one call per batch; catches a number another provider owns):
cURL
Upload the signed LoA PDF — pass ttl_ms long enough for the carrier to fetch it (24 h here; the default is 1 h and a slow carrier will see an expired URL):
cURL
200
Submit the port request with data.url from the upload:
cURL
201 returns data.id (prt_…). Ports complete with a carrier over 7–14 business days; track on GET /numbers/porting (one row per request) rather than polling porting/check. When the port lands, bind each new numbers row (it carries a num_… id) into the messaging service from loop 1. Canonical pages: Port numbers, First port walkthrough.

3. Rewire status callbacks into Orbit webhooks

Twilio POSTs form-encoded status callbacks to a statusCallback URL per message; Orbit emits a JSON envelope (message.delivered, message.failed, message.sent) to endpoints you register once under Settings → Webhooks. Subscribe to the event types, then re-point your handler — never translate the old per-message statusCallback URL field-for-field. Register the endpoint (once, tenant-wide):
cURL
201 returns data.id (wh_…) plus a one-time data.secret (whsec_…) — store it; the signature check needs it. The source-side form-encoded body (MessageSid=SM…&MessageStatus=delivered&…) becomes a JSON event — verify X-Orbit-Signature (HMAC-SHA256 over the raw body) before trusting it:
message.delivered event
Decision table for the verifier: Canonical pages: Webhook events, Verify webhook signatures, Webhook endpoint operations.

4. Flip the source IVR XML into a published flow

TwiML is a one-response XML document; Orbit models IVRs as IVR flows — a { nodes, edges } graph the visual builder also saves. The mapping is a graph with exactly one ivrStart entry node, the prompts/transfers/hangups the XML declared, and no unreachable nodes. Create a draft, then publish it so inbound callers hit it. Create the draft flow:
cURL
201 returns the draft with data.id (flw_…). The graph is validated on save — exactly one ivrStart, at least one terminal (transfer, hangup, voicemail, ringGroup, …), no unreachable nodes, and DTMF keys restricted to 0–9, *, #. transfer.transferTo must be a phone number or an on-net extension, never a raw sip: URI — outbound voice exits only via the Devotel softswitch. Publish so inbound callers hit it (a draft is invisible to traffic):
cURL
200 returns the new live version number. Prefer to hand-author? The dashboard Voice → IVR Builder canvas posts the same flow and publishes from the UI. Canonical pages: Build IVR flow, IVR builder canvas vs DSL walkthrough.

5. Swap the verify-codes round trip

The source’s verify service (Twilio Verify, Sinch Verify, MessageBird Verify) generates, delivers, and expires an OTP; Orbit’s Verify API does the same with a two-call round trip your backend drives. Never store or compare codes tenant-side. Send — capture data.verification_id:
cURL
Check — the user’s answer plus the id from the send:
cURL
data.status — approved is the only pass. failed means the code didn’t match; retry with the same verification_id until attempts_remaining hits 0, then send a fresh one. expired means the TTL lapsed — send again. In sandbox, the test key simulates delivery; read the expected code from the verification’s dashboard row. Canonical pages: Verify in 30 minutes, Verify API.

Errors as recipes (the 429 / 422 branches a cutover script needs)

Branch on error.code — never on message text, which can drift. A customer-side retry wrapper honors the server-provided wait first, then falls back to a capped exponential backoff — the full pattern is at API error handling by example.

See also