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

# Conversations: the omnichannel thread model

> How Orbit models a conversation: one thread id that spans channels, the conv_ id surviving channel switches via resume-channel, ownership (assignees/queues/teams) attaching to the conversation, AI-agent session continuity, channel- vs conversation-scoped state, and lifecycle statuses (open/pending/active/snoozed/closed/archived/resolved).

# Conversations: the omnichannel thread model

A **conversation** is Orbit's omnichannel thread — the single object the inbox, the AI agent, and your integrations all agree on. This page names the model the [API reference](/api-reference/conversations) endpoints operate on: what a conversation is versus a message and a contact, why the thread id survives channel switches, what attaches to the conversation versus the channel, and how lifecycle statuses interact with new inbound.

## Conversation, message, contact

Three distinct objects:

* A **contact** (`cnt_…`) is the person the thread is with. Identity — phone numbers, email addresses, widget sessions — lives on the contact.
* A **message** (`msg_…`) is one turn in the thread, sent or received on one channel.
* A **conversation** (`conv_…`) is the thread that owns the messages.

The model is **thread-first**: when you talk to a contact on more than one channel, all of those messages belong to one conversation. A channel is a label on each message, not a separate thread — the conversation owns every channel the thread has ever touched in its `channels` array. The one current channel — which the next reply defaults to — is the conversation's `channel` field. Routing and identity travel between thread and contact; ordering and history stay thread-scoped.

```json theme={null}
{
  "id": "conv_abc123",
  "contact": {
    "id": "cnt_9f8a",
    "name": "Maria Santos",
    "phone": "+14155552671",
    "email": "maria@example.com"
  },
  "channel": "email",
  "channels": ["sms", "email"],
  "status": "open",
  "assignee_id": "usr_7d21",
  "tags": ["billing"],
  "messages": [
    {
      "id": "msg_01",
      "direction": "inbound",
      "channel": "sms",
      "body": "Where is my invoice?",
      "created_at": "2026-08-22T09:14:02Z"
    },
    {
      "id": "msg_02",
      "direction": "outbound",
      "channel": "sms",
      "body": "Checking for you now.",
      "created_at": "2026-08-22T09:16:11Z"
    },
    {
      "id": "msg_03",
      "direction": "inbound",
      "channel": "email",
      "body": "Sent the details by email too.",
      "created_at": "2026-08-22T10:02:47Z"
    }
  ]
}
```

SMS and email messages sit in the same thread; the contact's phone and email identities both live on the contact.

## The thread id survives channel switches

Moving a thread between channels moves the **reply pointer**, never the thread. `POST /conversations/{id}/resume-channel` rebinds the conversation onto a new channel and returns where it came from plus every channel it has touched:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/conversations/conv_abc123/resume-channel \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{"target_channel": "email", "reason": "Customer asked to switch to email"}'
```

```json theme={null}
{
  "data": {
    "id": "conv_abc123",
    "channel": "email",
    "previous_channel": "sms",
    "channels": ["sms", "email"],
    "agent_active": true,
    "agent_id": "agent_42",
    "already_on_channel": false
  }
}
```

* `previous_channel` — the channel the thread was on before the move (`null` when there was none).
* `channels` — the union of every channel this thread has touched, including the new one.
* `already_on_channel` — `true` when the request repeated the current channel, making the endpoint idempotent.

The how-to walkthrough is the [continue-conversation guide](/guides/continue-conversation-across-channels); this page names the model that guide operates.

## Ownership attaches to the conversation

Assignees, queues, and teams bind to the thread, not to a channel. An assignment is `POST /conversations/{id}/assign` on the conversation id; the [handoff](/api-reference/endpoints/conversations) either places the thread into a team's queue or targets an individual agent. Channel moves keep the assignee and queue untouched — a conversation assigned to a teammate stays assigned after `resume-channel`, and routing rules re-evaluate the same thread regardless of which channel arrived last.

## The AI-agent session carries across channels

When an [AI agent](/agents/overview) is attached, it attaches to the conversation, not the channel. The session-check endpoint `GET /conversations/{id}/agent/active` reports `{ agent_active, agent_id }`; the `resume-channel` response echoes both so a caller can confirm the session moved with the thread. Because the thread is one object, the agent's memory and context travel with it — cross-channel continuation does not restart the agent.

## Channel-scoped versus conversation-scoped

The move boundary is explicit, and it is the same boundary the API exposes:

| Channel-scoped (what `resume-channel` changes)             | Conversation-scoped (what it never touches) |
| ---------------------------------------------------------- | ------------------------------------------- |
| The `channel` field — where the next reply goes by default | Full message history                        |
| `previous_channel` for the move audit                      | Tags, custom fields, follow state           |
| The `channels[]` union gains the new channel               | Assignee, queue, team ownership             |
|                                                            | AI-agent session and memory                 |
|                                                            | Lifecycle status (open/snoozed/…)           |

Outbound delivery still picks the channel per message — the `channel` field is only the default. Nothing else about the thread re-scopes when the default moves.

## Programmatic threading: `conversation_id`

By default, every outbound send threads itself: the channel send endpoints (`POST /messages/sms`, `/messages/whatsapp`, `/messages/email`, and the other `/messages/*` routes) run an implicit recipient-based auto-upsert that matches the send onto the contact's existing open conversation for that channel, or creates one when none exists. One active thread per `(channel, recipient)` pair — that is why the thread model above "just works" without any id on the send.

When the auto-upsert ambiguity bites — a contact with multiple open conversations, or a multi-channel contact whose reply must land on an exact thread — pass `conversation_id` with the id of an existing `conv_…` conversation on the send body:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/messages/sms \
  -H "X-API-Key: dv_test_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14155552671",
    "body": "Your refund is on its way.",
    "conversation_id": "conv_abc123"
  }'
```

The id points at an existing conversation. Orbit validates it before stamping the thread onto the message: a `conversation_id` that does not exist in your tenant is rejected with a `404`, and one that exists but addresses a different recipient or channel than the send is rejected with a `422` — the message is never silently re-threaded onto a foreign conversation. Omitting the field preserves the default auto-upsert behavior exactly.

Passing `conversation_id` matters in three situations:

* A contact has more than one open conversation and the reply must land on the intended thread, not whichever open thread the recipient match finds first.
* A contact is reachable on several channels at once, and the send must continue a specific thread rather than the current default one.
* Your integration stores its own thread mapping and wants deterministic landing instead of re-deriving it from auto-upsert rules.

Find the id on any conversation read — `GET /conversations`, `GET /conversations/{id}`, or the `conversation_id` filter on `GET /messages` once a thread exists.

## Lifecycle statuses and new inbound

A conversation moves through a fixed status set:

| Status                           | Class     | Meaning                                                                       |
| -------------------------------- | --------- | ----------------------------------------------------------------------------- |
| `open`, `pending`, `active`      | live      | The thread is workably live; new inbound and replies land here.               |
| `snoozed`                        | transient | Hidden until a wake-up time; auto-reopens when the deadline passes.           |
| `closed`, `archived`, `resolved` | terminal  | Moved out of the inbox; `archived` is terminal and cannot transition further. |

`closed` conversations can be reopened but cannot be continued onto another channel — reopen first, then `resume-channel`. Closed, archived, and snoozed threads reject continuation with a validation error rather than silently succeeding. `resolved` is the terminal sibling of `closed` and the two are interchangeable on read; treat both as "not live." New inbound on a live-channel thread re-keys against the contact's open conversation; terminal threads stay closed until `POST /conversations/{id}/reopen` (or the snooze deadline) brings them back.

## Scope of this page

This page names the model only. It does not walk through the channel-switch steps (that is the [continue-conversation guide](/guides/continue-conversation-across-channels)) and it does not document the request/response schemas per endpoint (that is the [conversations API reference](/api-reference/conversations)). Read those for the how and the contract; read this for the mental model both assume.

## Related reading

* [Conversations API reference](/api-reference/conversations) — every endpoint on the surface.
* [Continue a conversation across channels](/guides/continue-conversation-across-channels) — the operational walkthrough.
* [Data model](/concepts/data-model) — envelope, id, and pagination conventions shared across endpoints.
* [Tenant isolation](/concepts/tenant-isolation) — why thread ids never cross tenant boundaries.
