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

# Import conversations from signed IETF vCon containers

> Accept a signed IETF vCon artifact back into your workspace as a new conversation thread — signature-verified with your tenant's export secret before anything is stored, so a thread handed to a carrier, external AI system, or compliance archive returns as a portable, provenance-preserving handoff.

# Import conversations from signed IETF vCon containers

The [export guide](/guides/conversation-export-vcon) covers the outbound direction: turning one of your conversations into a signed vCon container. This guide covers the reverse direction. A carrier, an external AI system, or a compliance archive that received (or produced) a signed vCon can hand the artifact back to Orbit, and the envelope becomes a real thread in your omnichannel inbox — parties, per-turn dialog, and subject preserved exactly as the signer bound them.

Import is signature-gated. The artifact is verified against your tenant's shared export secret before any row is written, so a tampered or unsigned container never touches the workspace.

## The signed envelope

The request body is the vCon container itself. Orbit accepts the `"0.3.0"` draft track of the IETF vCon container — the same shape `GET /api/v1/conversations/:id/vcon` exports.

```json theme={null}
{
  "vcon": "0.3.0",
  "uuid": "8kPq3mZ1vQjR4sT7wY2x9A",
  "created_at": "2026-08-24T10:02:00.000Z",
  "subject": "Alice Example",
  "parties": [
    { "party": 0, "tel": "+14155552671", "name": "Alice Example", "role": "customer" },
    { "party": 1, "name": "Orbit", "role": "agent" }
  ],
  "dialog": [
    {
      "type": "text",
      "start": "2026-08-24T10:00:00.000Z",
      "party": 0,
      "mimetype": "text/plain",
      "body": "My order never arrived.",
      "encoding": "none",
      "meta": { "channel": "whatsapp", "direction": "inbound" }
    }
  ],
  "attachments": [],
  "analysis": [],
  "signatures": [
    {
      "protected": { "alg": "Orbit-HS256", "crit": ["alg"] },
      "payload_b64": "eyJ2Y29uIjoiMC4zLjAiLCJ1...",
      "signature": "tKb4LmQ7xR2vYp9..."
    }
  ]
}
```

Every field at the top level is optional except `uuid`:

| Field         | Import behavior                                                                                                                                                                                 |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `uuid`        | **Required.** 1–128 characters of base64url alphabet (`A-Z a-z 0-9 - _`). The artifact's tamper-evident public identifier — it must match the `uuid` inside the signed payload.                 |
| `vcon`        | Container version string. Accepted as-is; Orbit exports `"0.3.0"`.                                                                                                                              |
| `created_at`  | RFC 3339 timestamp of the original export. Recorded in the import audit row.                                                                                                                    |
| `subject`     | Up to 500 characters. Becomes the conversation label; when blank, the label falls back to `"<channel> thread <new conversation id>"`.                                                           |
| `parties`     | Party objects with `party` (non-negative index), `tel`, `mailto`, `name`, and `role`. An empty array is valid — the contact fields stay unset and the thread imports with no party identifiers. |
| `dialog`      | One item per turn, up to 500 items. Any turn may carry `type`, `start`, `party`, `mimetype`, `body`, `url`, and a free-form `meta` object. Turns beyond the 500th are not imported.             |
| `attachments` | Accepted for round-trip fidelity; not materialized into workspace rows.                                                                                                                         |
| `analysis`    | Accepted for round-trip fidelity; not materialized into workspace rows.                                                                                                                         |
| `signatures`  | At least one signature block. The first block must declare `protected.alg: "Orbit-HS256"` and carry the baseurl64 `payload_b64` plus the base64url HMAC-SHA256 `signature`.                     |

## Request and response

Send the container as the JSON body of a single POST. The request needs the `conversations:write` scope and an API key from a workspace member with write permission:

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/conversations/import-vcon" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d @handover.vcon.json
```

A verified import returns `201 Created`:

<ResponseExample>
  ```json theme={null}
  {
    "data": {
      "conversation_id": "cnv_01J8IMPORTED",
      "conversation": {
        "id": "cnv_01J8IMPORTED",
        "channel": "whatsapp",
        "channels": ["whatsapp"],
        "contact_phone": "+14155552671",
        "contact_email": null,
        "contact_name": "Alice Example",
        "status": "open",
        "created_at": "2026-08-24T10:15:00.000Z",
        "updated_at": "2026-08-24T10:15:00.000Z"
      },
      "artifact_uuid": "8kPq3mZ1vQjR4sT7wY2x9A",
      "imported_dialog_items": 24
    },
    "meta": { "request_id": "req_01J9EXAMPLE", "timestamp": "2026-08-24T10:15:00.000Z" }
  }
  ```
</ResponseExample>

The response tells you the new conversation id, echoes the artifact's `uuid`, and reports how many dialog turns were written. The conversation is created in `open` status and appears in your inbox immediately. Every import is recorded in your audit log with the artifact `uuid` and turn count — never with the party addresses themselves.

## Signature verification

Import re-runs the same verification the export guide documents, with your tenant's shared export secret, before anything is stored:

1. The first `signatures` block must declare `protected.alg: "Orbit-HS256"` — a container signed with any other algorithm label is rejected as invalid. There is no cross-alg acceptance.
2. The HMAC-SHA256 over the `payload_b64` string, keyed with the shared secret, must equal the block's `signature` byte for byte.
3. The decoded signed payload's `uuid` must equal the container's top-level `uuid`, which defeats substituting a different validly-signed payload onto the same envelope.

All three conditions, or the request fails. A stripped signature array, a signature that does not recompute, or a payload whose `uuid` was swapped all land in the same place: `422` with `VCON_SIGNATURE_INVALID`, and nothing is written.

Because verification is symmetric (your tenant's HMAC secret), only parties you handed the secret to can produce an importable artifact. If you rotate the platform signing secret, artifacts signed under the previous value stop verifying — re-export from the origin workspace, or have your counterparty re-sign with the current secret, before re-importing. Verifying by `payload_b64` recompute (rather than by indexing signatures per artifact `uuid`) also means there is no key id to pin: the current secret either recomputes the signature or the artifact is rejected.

Verification never throws on garbage input — unsigned, malformed, and tampered artifacts all return the same clean 422. An artifact that passes verification is then written with the exact bytes the signature bound; the import side applies no PII re-scan of its own, because re-scrubbing after signing would silently forge the signature.

## Identity and dedupe semantics

Import is a provenance-preserving handoff, not a live inbound message — so it never folds into an existing thread:

* **Every import mints a fresh conversation id.** The importer does not run the inbox's find-or-create continuity resolver, which would otherwise fold a phone/email match onto an open thread. The artifact's parties, timestamps, and turns land on a new row, byte-identical to the artifact.
* **Re-importing the same artifact creates a second thread.** There is no uuid-level idempotency by design — two imports of one artifact are two provenance-distinct handoffs. Each attempt is recorded separately in your audit log with the same artifact `uuid`, so duplicate imports are observable if you need to detect them.
* **The artifact `uuid` is not the conversation id.** The response's `artifact_uuid` identifies the envelope you sent; `conversation_id` is the new workspace row. Track the pair (`uuid`, `created_at`) as the artifact's identity.

## Party resolution

The importer projects the artifact's parties onto the new conversation's contact fields:

* **Party 0 is the customer.** Its `tel` and `mailto` become the conversation's contact phone and email, and its `name` the contact name. When the artifact carries no explicit `party: 0` entry, the first party in array order is treated as the customer.
* **Party 1 is the agent side.** Its `name` is echoed on the import audit row. When no `party: 1` exists, the first party after the customer is used.
* **Null-safe parties.** A party object may carry any subset of `tel`, `mailto`, and `name`; blank strings are dropped. An artifact with an empty `parties` array still imports — the conversation's contact fields stay unset and the `subject` becomes its only label.
* **Channel recovery.** The importer reads the channel label from the first dialog turn's `meta.channel` and accepts only the workspace channel vocabulary — `sms`, `whatsapp`, `email`, `instagram`, `messenger`, `rcs`, `viber`, `line`, `telegram`, `voice`, `video`, `web_chat`, `apple_messages`, `wechat`, `kakao`, `zalo`. Any other value collapses to the neutral `sms` default, so an artifact cannot write an arbitrary channel label into a real row.

## Errors to plan for

| Status        | Code                          | Cause                                                                                                                                                                                                                    | What to do                                                                           |
| ------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ |
| `422`         | `VALIDATION_ERROR`            | The body failed shape validation — a missing or invalid `uuid`, a party object with a negative index, more than 500 dialog items, or a malformed field value. The error body carries `details.issues` per failing field. | Fix the field named in `details.issues` and retry. No rows were written.             |
| `422`         | `VCON_SIGNATURE_INVALID`      | The artifact is unsigned, signed with a different algorithm, signed under a secret that has since rotated, or tampered. Rejected before any write.                                                                       | Re-export or re-sign against the current secret, then retry.                         |
| `422`         | `VCON_IMPORT_INVALID_CONTENT` | The artifact verified, but a dialog turn's content is data-invalid — most often a `start` value that is not a timestamp. The whole import rolls back atomically; no partial thread survives.                             | Repair the offending turn (for example normalise `start` to RFC 3339 UTC) and retry. |
| `404`         | `NOT_FOUND`                   | The workspace is not yet provisioned for imports.                                                                                                                                                                        | Retry once provisioning completes.                                                   |
| `401` / `403` | auth errors                   | Missing API key, missing the `conversations:write` scope, or a member role without write permission.                                                                                                                     | Check the key and role, then retry.                                                  |

Signature failures are the most common first-integration error. If every import returns `VCON_SIGNATURE_INVALID`, confirm the artifact's `protected.alg` is exactly `Orbit-HS256` and that the artifact was signed with the workspace's current secret — a rotated secret invalidates every older artifact.

## Fan-out guardrails

* **500-turn cap.** Import accepts at most 500 dialog items per request — the same bound the export side applies to a transcript. Longer histories need multiple artifacts.
* **Batched writes.** The conversation row and every dialog turn land in one database transaction, with turns inserted in batches. A malformed turn rolls the entire import back atomically — no half-imported threads ever survive.
* **No outbound traffic of any kind.** Import writes rows in your workspace and nothing else; it issues no SMS, voice, or provider call. The imported thread starts its inbox life in `open` status and picks up normal assignment and notification behavior from there.
* **Rate limits.** The route shares the standard write-rate budget. For bulk migrations, pace your imports and rely on the audit log (one row per attempt, keyed by artifact `uuid`) to reconcile.

## Worked examples

<CodeGroup>
  ```bash curl theme={null}
  # Import a signed vCon artifact exported from another workspace.
  # $ORBIT_API_KEY needs the conversations:write scope.
  curl -X POST "https://api.orbit.devotel.io/api/v1/conversations/import-vcon" \
    -H "X-API-Key: $ORBIT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "vcon": "0.3.0",
      "uuid": "8kPq3mZ1vQjR4sT7wY2x9A",
      "created_at": "2026-08-24T10:02:00.000Z",
      "subject": "Alice Example",
      "parties": [
        { "party": 0, "tel": "+14155552671", "name": "Alice Example", "role": "customer" },
        { "party": 1, "name": "Orbit", "role": "agent" }
      ],
      "dialog": [
        {
          "type": "text",
          "start": "2026-08-24T10:00:00.000Z",
          "party": 0,
          "mimetype": "text/plain",
          "body": "My order never arrived.",
          "encoding": "none",
          "meta": { "channel": "whatsapp", "direction": "inbound" }
        },
        {
          "type": "text",
          "start": "2026-08-24T10:01:30.000Z",
          "party": 1,
          "mimetype": "text/plain",
          "body": "I am sorry to hear that — let me check.",
          "encoding": "none",
          "meta": { "channel": "whatsapp", "direction": "outbound" }
        }
      ],
      "attachments": [],
      "analysis": [],
      "signatures": [
        {
          "protected": { "alg": "Orbit-HS256", "crit": ["alg"] },
          "payload_b64": "eyJ2Y29uIjoiMC4zLjAi...",
          "signature": "tKb4LmQ7xR2vYp9..."
        }
      ]
    }'
  ```

  ```javascript Node.js theme={null}
  import Orbit from "@devotel/sdk-node";

  const orbit = new Orbit({ apiKey: process.env.ORBIT_API_KEY });
  const artifact = await readFile("handover.vcon.json", "utf8");

  const result = await orbit.request(
    "POST",
    "/api/v1/conversations/import-vcon",
    { body: JSON.parse(artifact) }
  );
  console.log(result.data.conversation_id); // cnv_01J8IMPORTED
  ```

  ```python Python theme={null}
  import json
  import os
  import requests

  with open("handover.vcon.json") as fh:
      artifact = json.load(fh)

  resp = requests.post(
      "https://api.orbit.devotel.io/api/v1/conversations/import-vcon",
      headers={"X-API-Key": os.environ["ORBIT_API_KEY"]},
      json=artifact,
  )
  resp.raise_for_status()
  print(resp.json()["data"]["conversation_id"])  # cnv_01J8IMPORTED
  ```

  ```go Go theme={null}
  package main

  import (
  	"bytes"
  	"fmt"
  	"io"
  	"net/http"
  	"os"
  )

  func main() {
  	artifact, _ := os.ReadFile("handover.vcon.json")
  	req, _ := http.NewRequest(
  		"POST",
  		"https://api.orbit.devotel.io/api/v1/conversations/import-vcon",
  		bytes.NewReader(artifact),
  	)
  	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
  	req.Header.Set("Content-Type", "application/json")
  	resp, _ := http.DefaultClient.Do(req)
  	body, _ := io.ReadAll(resp.Body)
  	fmt.Println(string(body)) // {"data":{"conversation_id":"cnv_01J8IMPORTED", ...}}
  }
  ```

  ```php PHP theme={null}
  <?php
  $artifact = file_get_contents("handover.vcon.json");
  $ch = curl_init("https://api.orbit.devotel.io/api/v1/conversations/import-vcon");
  curl_setopt($ch, CURLOPT_POST, true);
  curl_setopt($ch, CURLOPT_POSTFIELDS, $artifact);
  curl_setopt($ch, CURLOPT_HTTPHEADER, [
      "X-API-Key: " . getenv("ORBIT_API_KEY"),
      "Content-Type: application/json",
  ]);
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  $response = curl_exec($ch);
  $data = json_decode($response, true);
  echo $data["data"]["conversation_id"]; // cnv_01J8IMPORTED
  ```

  ```ruby Ruby theme={null}
  require "net/http"
  require "json"

  artifact = File.read("handover.vcon.json")
  uri = URI("https://api.orbit.devotel.io/api/v1/conversations/import-vcon")
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true
  request = Net::HTTP::Post.new(uri, {
    "X-API-Key" => ENV["ORBIT_API_KEY"],
    "Content-Type" => "application/json",
  })
  request.body = artifact
  response = http.request(request)
  puts JSON.parse(response.body).dig("data", "conversation_id")
  ```

  ```csharp C# theme={null}
  using System.Net.Http.Headers;

  var artifact = await File.ReadAllTextAsync("handover.vcon.json");
  var client = new HttpClient();
  var request = new HttpRequestMessage(
      HttpMethod.Post,
      "https://api.orbit.devotel.io/api/v1/conversations/import-vcon");
  request.Headers.Add("X-API-Key",
      Environment.GetEnvironmentVariable("ORBIT_API_KEY"));
  request.Content = new StringContent(
      artifact, null, "application/json");
  var response = await client.SendAsync(request);
  Console.WriteLine(await response.Content.ReadAsStringAsync());
  ```
</CodeGroup>

## Migration flows that use import

* **Carrier handoff** — the receiving carrier exports a thread as a signed vCon and provisions your shared secret out of band; the artifact verifies on import and lands as a live thread.
* **External AI system** — a vCon-aware tool returns an annotated container; importing it back keeps the original parties and turn order intact on a new, provenance-distinct thread.
* **Compliance archive restore** — an archived export can be brought back as an active thread when a dispute or audit reopens; the signature proves the restored record is byte-identical to the archived one.

## Limits to plan for

* **Two-party shape.** Party 0 resolves to the customer, party 1 to the agent side. The `party_history` extension for multi-party containers is not used on import.
* **Attachments and analysis are carried, not written.** They survive the round-trip for fidelity but are not materialized into workspace rows.
* **No uuid-level idempotency.** Re-imports mint new threads deliberately; detect duplicates via the audit log's artifact `uuid` when your flow requires it.
* **Version track.** Imports follow the `"0.3.0"` draft track of the IETF container; expect the field to move forward as the draft matures.

## See also

* [Export conversations as signed vCon containers](/guides/conversation-export-vcon) — the outbound direction: creating the signed artifact an import consumes
* [Conversation archive](/guides/conversation-archive) — unsigned bulk CSV/JSON export across filtered result sets
* [Identity resolution](/guides/identity-resolution) — how Orbit matches party addresses on live inbound traffic
* [API keys end to end](/guides/api-keys-end-to-end) — provisioning a key with the `conversations:write` scope
