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 ishttps://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 migrateCLI — the same wizard scriptable from a terminal or pipeline. Install withnpm install -g @devotel/cli,devotel auth login, thendevotel migrate twilio --account-sid AC… --auth-token …(dry run first; add--runto 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
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
cURL
MessagingServiceSid:
cURL
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 asloaFileUrl. 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
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
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 astatusCallback 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
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 — capturedata.verification_id:
cURL
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 onerror.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
- Assisted import wizard — the dashboard wizard and
devotel migrateCLI these loops wrap for an operator - Devotel CLI guide — the scriptable loop (
devotel migrate twilio …) - Migration from Twilio — the provider-specific walkthrough these loops generalize
- Migration from Sinch / MessageBird / Vonage / Infobip — the siblings these loops re-express
- Migration playbook hub — the read that links all five providers
- Port numbers — loop 2’s full walkthrough (port-in plus port-out)
- Webhook events — loop 3’s event catalog
- Build IVR flow — loop 4’s full graph-builder walkthrough
- Verify in 30 minutes — loop 5’s onboarding walkthrough
- API error handling by example — the
429/422branches above expanded - API recipes: operations endpoints — the sibling cookbook for operational surfaces