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

# Imports API: one-click migration from another provider

> Migrate phone numbers, contacts, conversations, templates, and audiences from Twilio, Telnyx, Klaviyo, MessageBird, and Front — connect, preview, run, and track each import job.

# Imports API

The one-click migration importer pulls your existing configuration and audience out of another provider and maps it onto Orbit. It runs as a five-step wizard: **connect** a source account, **preview** what will be imported (a dry run), **run** the import as a background job, **watch** its progress live, and **reconcile** any conflicts.

Every credential you supply is used **only to read** your source account — phone numbers, messaging profiles, templates, contacts, conversations, and audiences. Orbit never sends messages or places calls through your old provider as a result of an import: outbound traffic always exits through Orbit. The credential is encrypted the moment you submit it, handed forward as an opaque `envelope` string, and removed from the job record once the import finishes.

**Base path:** `/api/v1/imports`

**Authentication:** Clerk session (`Authorization: Bearer <token>`) or API key (`X-API-Key`). Every endpoint requires an authenticated tenant.

***

## Supported sources

The `{source}` path segment on the dry-run and run endpoints is one of:

| Source             | Value         | Connect method                                             |
| ------------------ | ------------- | ---------------------------------------------------------- |
| Twilio             | `twilio`      | Twilio Connect OAuth, or a manual Account SID + Auth Token |
| Telnyx             | `telnyx`      | Manual V2 API key                                          |
| Klaviyo            | `klaviyo`     | Manual private API key (`pk_…`)                            |
| MessageBird (Bird) | `messagebird` | Manual access key                                          |
| Front              | `front`       | Connected during onboarding                                |

## Entity kinds

Each import selects a subset of entity kinds to pull. Every source advertises the kinds it supports; ask for between 1 and 10 per import.

`phone_numbers`, `messaging_services`, `templates`, `contacts`, `conversations`, `channels`, `flows`, `inboxes`, `tags`, `teammates`, `lists`, `segments`

***

## Step 1 — connect a source

The connect step exchanges your source credentials for an opaque, encrypted `envelope` string. You pass that `envelope` to the dry-run and run steps; it is never stored in the browser and the raw secret is never echoed back.

### Start the Twilio Connect authorization

<Note>
  `GET /api/v1/imports/twilio/connect`
</Note>

Begins the Twilio Connect OAuth flow. Returns a Twilio authorization URL for the wizard to open in a popup, plus a `state` value to match against on the callback.

```bash cURL theme={null}
curl "https://api.orbit.devotel.io/api/v1/imports/twilio/connect" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

**200 OK**

```json theme={null}
{
  "data": {
    "authorizeUrl": "https://www.twilio.com/authorize?response_type=code&client_id=CN…&state=twoa_…&code_challenge=…&code_challenge_method=S256",
    "state": "twoa_1a2b3c4d5e6f"
  },
  "meta": { "request_id": "req_abc123", "timestamp": "2026-08-13T12:00:00Z" }
}
```

Returns `503 TWILIO_CONNECT_NOT_CONFIGURED` when Twilio Connect is not enabled on the environment — fall back to **Submit Twilio credentials manually** below.

### Complete the Twilio Connect authorization

<Note>
  `GET /api/v1/imports/twilio/callback`
</Note>

Twilio redirects the browser here after the user approves access. It validates the returned `state`, exchanges the authorization code for credentials, encrypts them, and issues a `302` redirect back into the import wizard with the `envelope` attached to the URL. You do not call this endpoint yourself — Twilio invokes it, and the raw access token is never returned to the browser.

Returns `400` when the authorization was denied or the `code`/`state` is missing or invalid.

### Submit Twilio credentials manually

<Note>
  `POST /api/v1/imports/twilio/manual-credentials`
</Note>

Use this when the OAuth flow is unavailable (for example, a self-hosted subaccount). Returns the encrypted `envelope` directly.

<ParamField body="accountSid" type="string" required>
  Twilio Account SID — starts with `AC` followed by 32 hex characters.
</ParamField>

<ParamField body="authToken" type="string" required>
  Twilio Auth Token, 8–256 characters.
</ParamField>

```bash cURL theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/imports/twilio/manual-credentials" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "accountSid": "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "authToken": "your_auth_token" }'
```

**200 OK**

```json theme={null}
{
  "data": {
    "envelope": "enc:…opaque…",
    "accountSid": "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  },
  "meta": { "request_id": "req_abc123", "timestamp": "2026-08-13T12:00:00Z" }
}
```

### Submit Telnyx / Klaviyo / MessageBird credentials manually

<Note>
  `POST /api/v1/imports/telnyx/manual-credentials`<br />
  `POST /api/v1/imports/klaviyo/manual-credentials`<br />
  `POST /api/v1/imports/messagebird/manual-credentials`
</Note>

These providers don't publish an OAuth delegation flow, so you supply an API key directly. All three take the same body and return the same `envelope` shape.

<ParamField body="apiKey" type="string" required>
  The provider API key. Telnyx V2 keys start with `KEY`; Klaviyo private keys start with `pk_`; MessageBird access keys are alphanumeric (optionally prefixed `live_` or `test_`).
</ParamField>

<ParamField body="accountId" type="string">
  Optional display-only label shown in the wizard's "Connected as" line. Up to 128 characters.
</ParamField>

```bash cURL theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/imports/telnyx/manual-credentials" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "apiKey": "KEYxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "accountId": "Acme Corp" }'
```

**200 OK**

```json theme={null}
{
  "data": { "envelope": "enc:…opaque…", "accountId": "Acme Corp" },
  "meta": { "request_id": "req_abc123", "timestamp": "2026-08-13T12:00:00Z" }
}
```

Returns `422 VALIDATION_ERROR` when the key doesn't match the provider's format.

***

## Step 2 — preview (dry run)

<Note>
  `POST /api/v1/imports/{source}/dry-run`
</Note>

Counts the upstream entities and detects conflicts with data already in Orbit, so you can show an accurate row count and estimated duration before committing. Nothing is written.

<ParamField body="envelope" type="string" required>
  The encrypted credentials envelope from step 1.
</ParamField>

<ParamField body="entities" type="array" required>
  1–10 entity kinds to preview (see [Entity kinds](#entity-kinds)).
</ParamField>

<ParamField body="conversationDays" type="integer">
  Twilio only — how many days of conversation history to include, 1–90. Twilio exposes at most 90 days of message history without an Insights subscription, so values above 90 are rejected.
</ParamField>

```bash cURL theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/imports/twilio/dry-run" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "envelope": "enc:…opaque…",
    "entities": ["phone_numbers", "contacts", "conversations"],
    "conversationDays": 30
  }'
```

**200 OK**

```json theme={null}
{
  "data": {
    "source": "twilio",
    "entities": [
      { "kind": "phone_numbers", "estimatedCount": 12, "knownConflicts": 1 },
      { "kind": "contacts", "estimatedCount": 8421, "knownConflicts": 0 },
      { "kind": "conversations", "estimatedCount": 15230, "knownConflicts": 0 }
    ],
    "etaSeconds": 540,
    "generatedAt": "2026-08-13T12:00:00Z"
  },
  "meta": { "request_id": "req_abc123", "timestamp": "2026-08-13T12:00:00Z" }
}
```

***

## Step 3 — run the import

<Note>
  `POST /api/v1/imports/{source}/run`
</Note>

Queues a background import job and returns its `jobId`. Submit the same `envelope` and `entities` you previewed, optionally with a per-entity `conflictPolicy`.

<ParamField body="envelope" type="string" required>
  The encrypted credentials envelope from step 1.
</ParamField>

<ParamField body="entities" type="array" required>
  1–10 entity kinds to import.
</ParamField>

<ParamField body="conversationDays" type="integer">
  Twilio only — conversation lookback window, 1–90 days.
</ParamField>

<ParamField body="conflictPolicy" type="object">
  Optional map of entity kind → how to resolve a collision: `skip`, `overwrite`, or `merge`. Kinds you omit fall back to skipping conflicting rows.
</ParamField>

```bash cURL theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/imports/twilio/run" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "envelope": "enc:…opaque…",
    "entities": ["phone_numbers", "contacts", "conversations"],
    "conversationDays": 30,
    "conflictPolicy": { "contacts": "merge" }
  }'
```

**202 Accepted**

```json theme={null}
{
  "data": { "jobId": "imp_9s2h1k4m7p", "status": "pending" },
  "meta": { "request_id": "req_abc123", "timestamp": "2026-08-13T12:00:00Z" }
}
```

Open the [progress stream](#step-4-track-progress) for `jobId` to watch it run.

***

## Step 4 — track progress

### Stream import job progress

<Note>
  `GET /api/v1/imports/{jobId}/progress`
</Note>

A [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) stream of the job's per-entity counts and status transitions. The first frame is a `snapshot` of the current state, so a page that subscribes late renders immediately; keepalive comments arrive every 15 seconds and the stream closes automatically after one hour. A job that has already finished replays its final `end` frame and closes.

Frame events: `snapshot`, `progress`, `status`, `end`.

```bash cURL theme={null}
curl -N "https://api.orbit.devotel.io/api/v1/imports/imp_9s2h1k4m7p/progress" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

```text theme={null}
event: snapshot
data: {"jobId":"imp_9s2h1k4m7p","status":"running","source":"twilio","progress":[…]}

event: progress
data: {"type":"progress","kind":"contacts","imported":4200,"total":8421}

event: end
data: {"status":"succeeded"}
```

Returns `403` when the request origin isn't allowed, or `404` when no such job exists for your tenant.

### Get an import job

<Note>
  `GET /api/v1/imports/{jobId}`
</Note>

A point-in-time snapshot of a single job — its source, status, and per-entity progress. Poll this if you don't need the live stream.

```bash cURL theme={null}
curl "https://api.orbit.devotel.io/api/v1/imports/imp_9s2h1k4m7p" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

**200 OK**

```json theme={null}
{
  "data": {
    "id": "imp_9s2h1k4m7p",
    "source": "twilio",
    "status": "running",
    "progress": [
      { "kind": "contacts", "total": 8421, "imported": 4200, "skipped": 0, "failed": 0 }
    ]
  },
  "meta": { "request_id": "req_abc123", "timestamp": "2026-08-13T12:00:00Z" }
}
```

### List recent import jobs

<Note>
  `GET /api/v1/imports`
</Note>

Returns your organization's recent import jobs, each with its source and status — used to render the import history table.

```bash cURL theme={null}
curl "https://api.orbit.devotel.io/api/v1/imports" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

**200 OK**

```json theme={null}
{
  "data": {
    "jobs": [
      { "id": "imp_9s2h1k4m7p", "source": "twilio", "status": "running" },
      { "id": "imp_7a1b2c3d4e", "source": "telnyx", "status": "succeeded" }
    ]
  },
  "meta": { "request_id": "req_abc123", "timestamp": "2026-08-13T12:00:00Z" }
}
```

***

## Step 5 — cancel or roll back

### Cancel a running import job

<Note>
  `POST /api/v1/imports/{jobId}/cancel`
</Note>

Requests cancellation of an in-flight job. The status flips to `cancelled` and the worker stops between pages on its next check. Only a job that is still `pending` or `running` can be cancelled.

```bash cURL theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/imports/imp_9s2h1k4m7p/cancel" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

Returns `404 NOT_FOUND` when the job doesn't exist, or `409 IMPORT_NOT_CANCELLABLE` (with the current status in `details.current_status`) when the job has already reached a terminal state.

### Roll back a completed import

<Note>
  `POST /api/v1/imports/{jobId}/rollback`
</Note>

Deletes every contact created by this import batch in a single transaction and records the rollback on the job. This is destructive and is restricted to **owner** or **admin** roles. Roll back only after the job has finished or been cancelled.

```bash cURL theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/imports/imp_9s2h1k4m7p/rollback" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

**200 OK**

```json theme={null}
{
  "data": { "rolledBack": true, "deletedCount": 8421 },
  "meta": { "request_id": "req_abc123", "timestamp": "2026-08-13T12:00:00Z" }
}
```

Returns `403 FORBIDDEN` for a non-owner/admin caller, `404 NOT_FOUND` when the job doesn't exist, and `409` when the job is still running (`IMPORT_STILL_RUNNING`) or has already been rolled back (`ALREADY_ROLLED_BACK`).

***

## Errors

| Status | `error.code`                                   | Cause                                                                                         |
| ------ | ---------------------------------------------- | --------------------------------------------------------------------------------------------- |
| `400`  | `INVALID_ENVELOPE`                             | The credentials `envelope` couldn't be decrypted — restart from step 1.                       |
| `400`  | `TWILIO_OAUTH_DENIED` / `VALIDATION_ERROR`     | The Twilio callback was denied, or `code`/`state` was missing.                                |
| `403`  | `FORBIDDEN`                                    | Only `owner` or `admin` may roll back an import.                                              |
| `403`  | `OAUTH_STATE_MISMATCH`                         | The OAuth `state` doesn't match the current session.                                          |
| `404`  | `UNKNOWN_IMPORT_SOURCE`                        | The `{source}` segment isn't a supported provider.                                            |
| `404`  | `NOT_FOUND`                                    | No import job with that id exists for your tenant.                                            |
| `409`  | `IMPORT_NOT_CANCELLABLE`                       | The job already reached a terminal status.                                                    |
| `409`  | `IMPORT_STILL_RUNNING` / `ALREADY_ROLLED_BACK` | Cancel before rolling back, or the job was already rolled back.                               |
| `422`  | `VALIDATION_ERROR`                             | The request body failed validation (bad key format, empty `entities`, or more than 10 kinds). |
| `503`  | `TWILIO_CONNECT_NOT_CONFIGURED`                | Twilio Connect isn't enabled — use manual credentials.                                        |

## See also

* [Migrating from Twilio](/guides/migration-from-twilio)
* [Contacts API](/api-reference/endpoints/contacts)
* [Numbers API](/api-reference/numbers)
