Errors worth branching on
These five failures cover the import job create + status poll (POST /api/v1/imports, GET /api/v1/imports/), hammered by every onboarding wizard. Each block below is a full{ error, meta } envelope as the API returns it, and the matrix at the bottom answers retry vs surface for the same five classes. For the platform-wide decision table these branches plug into, see the error handling guide.
401 — Unauthorized
401 on this page is the bearer key failing before the route ran — it never means the resource is wrong. Rotate the key or re-mint the scoped token; retrying the same request changes nothing.
403 — Forbidden
403 means the key authenticated but the operation is gated by scope — check the key’s scopes on the developer page; a 403 is never a data-not-found shape.
422 — Schema
422 means the payload did not match the request schema — branch on error.details.field and poll GET /api/v1/imports/ for the job state instead of re-posting the same payload instead of blind-retrying the same body.
429 — Rate
Retry-After header and error.details.retry_after are both set on every 429 — resend the SAME request after the lower of the two.
402 — Feature gate
60-second retry matrix
Worked request and response samples
Copy the request as written, substitute your own ids, and compare the response envelope — a success in the same shape the numbered error sections spell out.Start a contact import
POST /api/v1/importsWorked provider-import samples
A migration off Twilio, Telnyx, Klaviyo, or MessageBird is a five-step chain: hand over credentials, preview what the source holds, start the run, watch the job, and cancel or roll back if the preview lied. Every response below keeps the{ data, meta } envelope. The samples past the Twilio manual-credentials step are numbered cURL + Node.js pairs; the Node tab uses the orbit.request escape hatch because the SDK has no typed imports namespace yet — vendor it from packages/sdk-node, per the SDK index. For the wizard-side walkthrough of the same chain, see the import wizard guide.
Credentials are used to READ the source account only — no outbound voice or SMS is ever routed through the source provider as a result of the import; outbound traffic keeps exiting via Devotel’s wholesale softswitch.
1. Hand over the credentials — get back an opaque envelope
The run and dry-run calls never take raw provider keys. You exchange the provider credential ONCE for an encrypted envelope and pass that envelope forward. Use whichever intake the provider supports:- Twilio (manual fallback) — Account SID + Auth Token, when the OAuth flow below is unavailable.
- Telnyx — a V2 API key (starts with
KEY). - Klaviyo — a private API key (starts with
pk_). - MessageBird — an access key (
live_/test_prefixed or bare).
200
data.envelope into every dry-run and run call from here on. A malformed key fails the format check locally with a 422 before any upstream round-trip (see the 422 — Credentials branch below); Twilio additionally returns accountSid alongside the envelope.
2. Preview the import — dry-run before anything is written
POST /api/v1/imports/{source}/dry-runestimatedCount per entity, knownConflicts (rows already on the Orbit side — a phone number already in the pool, a contact already imported), and the etaSeconds total. The valid entity kinds are phone_numbers, messaging_services, templates, contacts, conversations, channels, flows, inboxes, tags, teammates, lists, segments — each source supports a subset, and unsupported kinds are rejected by schema. Twilio conversation history caps at 90 days; past that the provider returns nothing more. Nothing is written at this step.
3. Start the run — get a job id back
POST /api/v1/imports/{source}/runskip, overwrite, or merge).
jobId is the handle for every later step. Credentials ride the job as an encrypted envelope, are decrypted only inside the worker, and are redacted from the job record once the job reaches a terminal status.
4. Poll the job — point-in-time snapshot of progress
GET /api/v1/imports/{jobId}status walks pending → running → succeeded | failed | cancelled. Poll on a 2–5 second cadence, or subscribe to GET /api/v1/imports/{jobId}/progress for the same counters as a live Server-Sent Events stream (snapshot first frame, then progress/status events, then a terminal end event). A terminal job reports its per-row failures in progress[].failed and its collisions in progress[].conflicts — resolve those in the wizard’s reconcile step.
5. Prefer the OAuth path for Twilio (no secrets to paste)
The manual-credentials paste is the fallback. When Twilio Connect is available the flow is two browser steps and zero secrets through the clipboard.authorizeUrl, and the operator approves on Twilio’s page. Twilio then redirects the browser to the callback — you do not construct this URL yourself, Twilio does, with the authorization code and the single-use state from step A:
Step B — Twilio redirects the browser here (no client code needed)
302
envelope the manual path returns. A user who declines the consent lands on a 400 TWILIO_OAUTH_DENIED; a state older than 10 minutes or posted twice lands on 400 INVALID_OAUTH_STATE. When Twilio Connect is not configured at all, step A returns 503 TWILIO_CONNECT_NOT_CONFIGURED — fall back to the manual-credentials intake in step 1.
6. Cancel an in-flight job
POST /api/v1/imports/{jobId}/cancelpending or running jobs can be cancelled — cancelling a finished job returns the 409 IMPORT_NOT_CANCELLABLE shape below. Rows already imported stay imported; to undo a completed import, roll back instead.
7. Roll back a completed import — undo what it wrote
POST /api/v1/imports/{jobId}/rollback409 ALREADY_ROLLED_BACK; one still running returns 409 IMPORT_STILL_RUNNING (cancel it first). A non-owner/admin key gets the 403 shape from the error section above with the rollback-specific message.
8. Retries are safe — pin writes to an Idempotency-Key
Every mutating endpoint on this page accepts anIdempotency-Key header. Pass a stable, client-generated value and the second delivery of the same request replays the first response — the run is not enqueued twice.
Idempotency-Replay: true header and the original request_id — no second job. A retry that arrives while the first delivery is still in flight gets the 409 in-flight conflict below; back off and retry it (or simply re-poll GET /imports for the job). Key reuse is scoped per credential + path and held for 24h on 2xx (5 min on 4xx, 30 s on 5xx).
422 — Credentials
Past the generic schema case above, the per-provider credential intakes reject a key that fails the provider’s format rules with the check spelled out inerror.details. This fires locally — before any round-trip to the provider — so fix the paste and resend; it never becomes a retryable condition.
accountSid (must match AC + 32 hex) plus a minimum-length authToken; Klaviyo on apiKey starting pk_; MessageBird on a 20–128 character key with an optional live_/test_ prefix. Branch on error.details.field exactly as for the generic schema branch.
409 — In-flight idempotency conflict
The concurrency branch of the Idempotency-Key contract above: the retry beat the original delivery.GET /api/v1/imports for the job the first delivery created. Never rotate to a new key on this branch — a new key starts a second run. Two sibling 409s live on the terminal-side endpoints: IMPORT_NOT_CANCELLABLE (error.details.current_status tells you which terminal status blocked the cancel) and ALREADY_ROLLED_BACK / IMPORT_STILL_RUNNING on rollback. All three are stable states, not retries.