> For the complete documentation index, see [llms.txt](https://docs.amigo.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.amigo.ai/developer-guide/platform-api/conversations.md).

# Conversations

{% hint style="info" %}
Conversation detail is channel-aware, so voice calls and text conversations can be inspected through the same resource shape. Use the unified [Runs](/developer-guide/platform-api/conversations/runs.md) endpoint to browse activity across channels.
{% endhint %}

Conversations are first-class REST resources. You create them, send turns, inspect them, and close them when you are done. A separate WebSocket endpoint handles interactive streaming chat. Both use the same durable conversation store. A conversation first opened by the Sessions WebSocket can later be inspected or continued over REST by using the `conversation_id` from `session.created`; the WebSocket cannot select an independently created REST conversation by ID.

{% hint style="info" %}
**One reasoning system, transport-aware delivery.** REST, WebSocket, SMS, and WhatsApp share the conversation engine, tools, and safety rules, while channel-specific profiles still shape delivery behavior.
{% endhint %}

## API Surface

Conversation resources cover create, detail, turn submission, channel switching, and close operations. Sessions provide the public WebSocket surface for low-latency text conversations. Workspace-wide browsing is provided by [Runs](/developer-guide/platform-api/conversations/runs.md).

The REST operations are embedded in the relevant sections below. The [Sessions](/developer-guide/platform-api/platform-api/sessions.md) guide covers the separate WebSocket contract and close codes.

## Architecture

```mermaid
flowchart LR
    subgraph REST["REST API"]
        create["POST /conversations"]
        turn["POST /{id}/turns"]
        detail["GET /{id}"]
        close["DELETE /{id}"]
    end

    subgraph WS["WebSocket"]
        stream["WS /sessions/connect"]
    end

    turn -->|proxy| actor["Conversation Engine"]
    stream -->|streamed turns| actor
    actor <-->|durable state| store["Conversation Store"]
    actor <-->|tool calls| tools["Tools"]
    actor <-->|patient context| wm["World Model"]

    create --> store
    detail --> store
    close --> store
```

***

## Conversation Lifecycle

```mermaid
stateDiagram-v2
    [*] --> Active: Recent non-terminal activity
    Active --> Dormant: No update within channel threshold
    Dormant --> Active: New activity
    Active --> Closed: Terminal status or DELETE /{id}
    Dormant --> Closed: Terminal status or DELETE /{id}
    Closed --> [*]
```

| State       | What it means                                                                                                                                                                         |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Active**  | The stored status is non-terminal and `updated_at` is within the channel's activity threshold. This is a recency signal, not proof that a turn currently holds the conversation lock. |
| **Dormant** | The stored status is non-terminal and the conversation has not been updated within the channel's activity threshold. A later turn can make it active again.                           |
| **Closed**  | The stored status is `closed`, `completed`, or `failed`. No more turns are accepted.                                                                                                  |

The `lifecycle` field is derived when the conversation is read. It is not a stored engine state and must not be used as a lock or delivery acknowledgment.

{% hint style="info" %}
The stored `status` is one of `active`, `closed`, `completed`, `in-progress`, or `failed`. The separate derived `lifecycle` is `active`, `dormant`, or `closed`. For web conversations, the active-to-dormant recency threshold is 5 minutes.
{% endhint %}

### What Gets Saved

| Field      | What it contains                                                                                                                                      |
| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Plan**   | Natural-language summary of the conversation state, written by an LLM when turns are compressed. Survives platform upgrades without schema migration. |
| **Turns**  | Last 200 verbatim messages (role, text, timestamp).                                                                                                   |
| **Cursor** | Channel-specific position marker.                                                                                                                     |

### Session Timing (WebSocket)

| Setting                     | Value                                                       |
| --------------------------- | ----------------------------------------------------------- |
| Idle timeout                | 5 minutes while waiting for the next client text frame      |
| User-turn timeout           | 60 seconds                                                  |
| Maximum connection lifetime | No endpoint-specific maximum is part of the public contract |

An idle timeout sends an `idle_timeout` error and closes the WebSocket, but it does not close the durable conversation. A user-turn timeout sends `turn_timeout` and leaves the socket available for a later turn. The Sessions WebSocket does not accept a `stop` client message; use the Conversations close endpoint to close the durable record.

***

## REST API

### Create a Conversation

{% openapi src="<https://api.platform.amigo.ai/v1/openapi.json>" path="/v1/{workspace\_id}/conversations" method="post" %}
<https://api.platform.amigo.ai/v1/openapi.json>
{% endopenapi %}

Creates a conversation record and returns it. The conversation starts in a usable state - send your first turn immediately after.

For web conversations, the agent's greeting is produced as part of the first turn rather than at creation time: the create call returns the conversation with an empty `turns` array, and the opening exchange happens on your first `POST /turns`. For outbound channels, the create call itself dispatches the agent's opening message (see below).

#### Starting an Outbound Conversation

The create operation defaults to an inbound `web` conversation. Set an outbound channel to have the agent send the first message and return the durable conversation record. Outbound creation currently supports `sms` and `imessage`.

When `channel` is not `web`, the create dispatches the agent's first message through the messaging channel layer and returns the durable conversation. The routing and sending number are resolved server-side from `use_case_id` - never supplied by the caller. Carrier compliance registration is handled by the platform.

Requires the `Conversation:StartOutbound` permission (member, admin, or owner - **not** viewer or operator). Gates are evaluated in this order:

| Status | Meaning                                                                                 |
| ------ | --------------------------------------------------------------------------------------- |
| `403`  | External-user token, or the caller lacks `Conversation:StartOutbound`.                  |
| `501`  | `channel` not in `{sms, imessage}` (e.g. `email`, `whatsapp`, `voice`).                 |
| `422`  | Missing `recipient`, or missing `use_case_id`.                                          |
| `404`  | Service not owned by this workspace, or use case not owned by this workspace + service. |
| `503`  | Outbound conversations are not configured.                                              |

An inbound reply from the recipient re-converges onto this same conversation by construction - the platform derives the routing key from `(channel, recipient, use_case_id)` the same way on both the send and receive paths.

**Forcing a fresh conversation.** On thread-keyed channels (`sms`, `imessage`), pass `force_new: true` to close any existing active conversation on the target thread (recipient + use case) before the outbound opener is dispatched, so a brand-new conversation is materialized. `force_new` is rejected with `422` on `channel=web` - web conversations always start fresh.

***

### Send a Turn

{% openapi src="<https://api.platform.amigo.ai/v1/openapi.json>" path="/v1/{workspace\_id}/conversations/{conversation\_id}/turns" method="post" %}
<https://api.platform.amigo.ai/v1/openapi.json>
{% endopenapi %}

Send a message and get the agent's response. The platform loads the durable conversation state, runs the message through the reasoning engine, and persists the resulting turns and state.

The endpoint negotiates two response shapes by `Accept` header. The default is the synchronous JSON response. Pass `Accept: text/event-stream` to receive token-by-token output and tool-call telemetry as a typed SSE event stream - see [Streaming Turns](#streaming-turns-sse).

The response echoes your input, gives you the agent's output (can be multiple messages if the agent calls tools), and includes a snapshot of the conversation state. A 409 is returned when the conversation is closed **or** another turn is already being processed (see [Concurrency](#concurrency) below).

The response also carries a boolean **`background_pending`**. It is `false` on an ordinary turn: `output` is the complete answer. It is `true` when a tool the agent called crossed the server's blocking window and was handed to a background task - in that case `output` is only an acknowledgement ("Let me check that…"), **not** the final answer. See [Background (deferred) tool results](#background-deferred-tool-results).

**Turn identity.** The turn response carries a stable `turn_id` (UUID), and each message on it carries `turn_id` plus `turn_index` (zero-based ordinal of the user exchange). The same identity appears on the conversation's history turns, so `turn_id` can key durable per-turn artifacts - feedback, annotations - across page reloads and re-fetches. Both fields are `null` on messages that precede the first user exchange (proactive greetings, channel-event preludes).

***

### Background (deferred) tool results

Some tools take longer than the server will hold a turn open (for example a multi-step lookup or an external system call). When that happens the turn returns with `background_pending: true` and an acknowledgement in `output`; the tool continues outside the request window. Completion time depends on the tool and its dependencies. If continuation processing succeeds, the resulting answer is appended to durable conversation state; `background_pending` is not a guarantee that a final answer will eventually be produced.

Retrieve it in any of these ways (all on the same conversation, no second connection):

* **Send the next user turn.** A completed background result can be folded into that turn's reply.
* **Poll.** Re-issue `POST /{id}/turns?poll=true` with no message - a drain-and-report request that returns completed results and reports whether more work remains. A poll does not accept a message body, SSE streaming, or the per-turn wait/suppress controls (`422`).
* **Re-read the conversation.** `GET /{id}` reflects messages that have been durably persisted at read time. It cannot manufacture a missing result for work that is still pending or failed before persistence.

A client that treats a `background_pending: true` turn as complete can miss a later result. Poll (or send the next turn) until the API reports no pending work or an explicit failure or terminal outcome. If a turn's outcome is ambiguous, inspect the durable conversation before retrying.

Two optional per-turn request fields tune this behavior for synchronous callers:

* **`wait_for_final`** - when a turn hands work to a background tool, await the real answer inline (bounded, \~30s) instead of returning the acknowledgement immediately. On completion the response carries the final answer with `background_pending: false`; on timeout it returns `background_pending: true` - resolve it later with `poll=true`. `null` (default) inherits the channel policy (web = do not wait).
* **`suppress_filler`** - when a turn ends `background_pending`, omit the filler/acknowledgement text from `output` so a batch caller never mistakes the acknowledgement for the answer (`output` is empty; `background_pending: true` is the poll-later signal). `null` (default) inherits the channel policy.

{% hint style="info" %}
Do **not** reach for the workspace event stream (`GET /v1/{workspace_id}/events/stream`) to receive a single chat's background results. That stream is a workspace-wide observer bus for dashboards watching many conversations; it mirrors this activity as `text.tool_started`, `text.background_result`, and `text.agent_message` events, but the per-conversation contract above (drain / poll / re-read) is the supported delivery path for a chat client.
{% endhint %}

***

### Streaming Turns (SSE)

For low-latency UIs that display generated tokens and tool-call activity inline, use the dedicated streaming operation, which always returns `text/event-stream` regardless of the `Accept` header. The same stream is also available on the regular turns endpoint through content negotiation.

{% openapi src="<https://api.platform.amigo.ai/v1/openapi.json>" path="/v1/{workspace\_id}/conversations/{conversation\_id}/turns/stream" method="post" %}
<https://api.platform.amigo.ai/v1/openapi.json>
{% endopenapi %}

Example using the regular turns operation:

```
POST /v1/{workspace_id}/conversations/{conversation_id}/turns
Authorization: Bearer <api_key>
Accept: text/event-stream
Content-Type: application/json

{"message": "I need to schedule an appointment"}
```

The server keeps the connection open and emits a typed sequence of events (`event:` lines) terminated by `done` on success or `error` on failure. Concurrency and lock semantics match the JSON path. A disconnect before the terminal event has an ambiguous outcome, so read the durable conversation before retrying.

#### Example wire format

```
event: thinking
data: {"event":"thinking","tier":0,"tier_name":"empathy"}

event: token
data: {"event":"token","text":"I"}

event: token
data: {"event":"token","text":" can"}

event: tool_call_started
data: {"event":"tool_call_started","tool_name":"search_appointments","call_id":"c1","input":"{\"date\":\"2026-04-29\"}"}

event: tool_call_completed
data: {"event":"tool_call_completed","tool_name":"search_appointments","call_id":"c1","result":"[...]","succeeded":true}

event: message
data: {"event":"message","role":"agent","text":"I can help. I found 3 slots..."}

event: done
data: {"event":"done","conversation_id":"c60818df-...","status":"active","turn_count":2}
```

#### Streaming behavior

* The stream completes when a `done` event is received. Always treat `done` as the signal to close the reader; do not rely on connection-close alone.
* Render `message` events as the authoritative completed agent messages. `token` events are useful for progressive display, but a chat transcript should commit the final `message` event text.
* Tool-backed turns can emit `tool_call_started` and `tool_call_completed` before the final `message`. Keep the response UI in a pending state until the `message` and `done` events arrive.
* A mid-stream `error` event is terminal - no `done` follows. Inspect `message` for the failure reason and retry with backoff.
* If the client disconnects before `done`, do not infer whether the turn failed or completed from the disconnect alone. Read `GET /v1/{workspace_id}/conversations/{conversation_id}` before deciding whether to retry.
* Standard HTTP status codes (404, 409, 503) are returned **before** the stream begins. Once the response body starts, status is 200 and failures arrive as `error` events.

***

### Concurrency

REST turns are **serialized per conversation**. Only one turn can be in flight at a time for a given conversation. Different conversations can process turns simultaneously - the lock is per-conversation, not global.

| Scenario                                                | What happens                                                                                                       |
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| Send a turn while the previous turn is still processing | **409 Conflict** - `"Conversation is already active"`.                                                             |
| Send a REST turn while a WebSocket turn is processing   | **409 Conflict** - `"Conversation is already active"`. An idle open WebSocket does not hold the conversation lock. |
| Send a turn after the previous turn returned            | Works normally.                                                                                                    |
| Send a turn to a closed conversation                    | **409 Conflict** - `"Conversation is closed"`.                                                                     |
| `GET` the conversation while a turn is processing       | **Works** - read endpoints are never blocked.                                                                      |

#### Distinguishing "busy" from "closed"

Both return 409, but the response body differs:

```json
// Busy (another turn in flight)
{ "detail": "Conversation is already active" }

// Closed (terminal - no more turns accepted)
{ "detail": "Conversation is closed" }
```

Check the `detail` string to decide whether to retry or stop.

#### Retry strategy

On `409` with `"Conversation is already active"`, retry with bounded exponential backoff and jitter. `GET /conversations/{conversation_id}` can show whether new turns were persisted, but its derived `lifecycle` is a recency view rather than live lock ownership. Stop retrying if the conversation becomes terminal.

#### What if your HTTP client times out?

If your client times out, the outcome is ambiguous: the server may still complete and persist the turn. Read the conversation and inspect `lifecycle` and `turns` before retrying so you do not create a duplicate user turn.

#### Lock release timing

Conversation ownership is released when the turn finishes. After an interrupted request, a later turn can still return `409` while cleanup is in progress. The public REST surface does not provide an in-progress turn cancellation endpoint.

{% hint style="warning" %}
**Do not fire-and-forget concurrent turns.** Each REST or WebSocket turn must complete before the next one is sent. On WebSocket, wait for the current turn's terminal `done` or `error` event before sending the next `user_text`; buffered frames are not a durable queue or delivery acknowledgment.
{% endhint %}

***

### Browse Conversations

Use the unified runs inventory with `kind=conversation` to browse voice, text, SMS, email, and web activity. The retired conversation-list endpoint is no longer part of the public contract. Open a returned run's `source_conversation_id` with conversation detail when you need the full transcript.

{% openapi src="<https://api.platform.amigo.ai/v1/openapi.json>" path="/v1/{workspace\_id}/runs" method="get" %}
<https://api.platform.amigo.ai/v1/openapi.json>
{% endopenapi %}

***

### Switch Conversation Channel

Move an active conversation to a different channel. The durable conversation ID is preserved - only the routing changes. The same conversation continues on the new channel.

{% openapi src="<https://api.platform.amigo.ai/v1/openapi.json>" path="/v1/{workspace\_id}/conversations/{conversation\_id}/channel" method="post" %}
<https://api.platform.amigo.ai/v1/openapi.json>
{% endopenapi %}

Requires the `Conversation:SwitchChannel` permission (member tier and above). External users cannot switch channels.

Only `sms` and `imessage` are supported switch targets today; `email`, `whatsapp`, and `voice` return `501`. Voice conversations cannot be switched. A successful switch preserves history and the compressed plan, and later replies on the new channel continue the same conversation.

#### Error Responses

Gates are evaluated in this order:

| Status | Meaning                                                                                                                         |
| ------ | ------------------------------------------------------------------------------------------------------------------------------- |
| `403`  | External-user token, or the caller lacks `Conversation:SwitchChannel`.                                                          |
| `501`  | Target channel not in `{sms, imessage}` (e.g. `email`, `whatsapp`, `voice`).                                                    |
| `422`  | Missing `recipient` or `use_case_id`, or `dispatch_opener` is `true` but the conversation has no service.                       |
| `404`  | Use case not owned by this workspace, or conversation not found / already closed / not switchable (voice).                      |
| `409`  | Another active conversation already uses this `(channel, recipient)` combination.                                               |
| `503`  | Outbound conversations are not configured.                                                                                      |
| `502`  | The switch was persisted but the optional opener dispatch failed. The switch is durable; retry the opener without re-switching. |

***

## Get Conversation Detail

{% openapi src="<https://api.platform.amigo.ai/v1/openapi.json>" path="/v1/{workspace\_id}/conversations/{conversation\_id}" method="get" %}
<https://api.platform.amigo.ai/v1/openapi.json>
{% endopenapi %}

Returns the full conversation including turns and the compressed plan (if frozen).

{% hint style="info" %}
**Web chat transcript source.** For web/text conversations, render history from this endpoint's `turns[]` array. Each turn has `role`, `text`, and `timestamp`; agent messages appear as `{"role": "agent", "text": "..."}`. Do not use the call detail shape (`/calls/{id}` or `agent_transcript`) for web chat history. Call transcript fields are for call-oriented views and may not contain web/text agent messages.
{% endhint %}

{% hint style="info" %}
**Per-turn channel attribution.** Each turn carries a `channel` field (`web`, `sms`, `imessage`, and others), recording which channel delivered it. After a [channel switch](#switch-conversation-channel), earlier turns keep their original channel and later turns reflect the new one. The field may be `null` on older or internal turns.
{% endhint %}

***

### Close a Conversation

{% openapi src="<https://api.platform.amigo.ai/v1/openapi.json>" path="/v1/{workspace\_id}/conversations/{conversation\_id}" method="delete" %}
<https://api.platform.amigo.ai/v1/openapi.json>
{% endopenapi %}

Closes the conversation permanently. Subsequent turns return 409.

***

### Example: Full REST Flow

```bash
# 1. Create
CONV=$(curl -s -X POST https://api.platform.amigo.ai/v1/$WS/conversations \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"service_id": "'$SERVICE_ID'"}' | jq -r '.id')

# 2. First turn
curl -X POST https://api.platform.amigo.ai/v1/$WS/conversations/$CONV/turns \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"message": "I need to schedule an appointment"}'

# 3. Second turn
curl -X POST https://api.platform.amigo.ai/v1/$WS/conversations/$CONV/turns \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"message": "Next Tuesday at 2pm"}'

# 4. Check the full conversation
curl https://api.platform.amigo.ai/v1/$WS/conversations/$CONV \
  -H "Authorization: Bearer $TOKEN"

# 5. Close when done
curl -X DELETE https://api.platform.amigo.ai/v1/$WS/conversations/$CONV \
  -H "Authorization: Bearer $TOKEN"
```

***

## WebSocket Sessions

Use [Sessions](/developer-guide/platform-api/platform-api/sessions.md) for public bidirectional text conversations. The endpoint resolves one active thread per `(workspace_id, entity_id, service_id)` combination and returns its durable ID in `session.created`. Clients reconnect with the same service and entity rather than passing a conversation ID.

### Connect

```
WS /v1/{workspace_id}/sessions/connect?service_id={id}&entity_id={id}
Sec-WebSocket-Protocol: auth, <api_key_or_jwt>
```

| Parameter         | Location                        | Required | Description                                                                                                                                      |
| ----------------- | ------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `service_id`      | query                           | Yes      | Which agent (UUID)                                                                                                                               |
| `entity_id`       | query                           | Yes      | Patient entity for world model context (UUID)                                                                                                    |
| `tool_events`     | query                           | No       | Emit `tool_call_started` / `tool_call_completed` frames. Default `false`.                                                                        |
| `context`         | query                           | No       | Additional context for the opening turn; limited to 5,000 characters.                                                                            |
| `viewport_width`  | query                           | No       | Text width from 20 through 500 characters.                                                                                                       |
| `viewport_height` | query                           | No       | Text height from 5 through 500 characters; useful with `viewport_width`.                                                                         |
| *credential*      | `Sec-WebSocket-Protocol` header | Yes      | Two comma-separated values: the literal `auth` followed by an API key or JWT. The server echoes `auth` as the negotiated subprotocol on success. |

{% hint style="warning" %}
**Credentials never go in the URL.** Query-string tokens are not supported. Browsers can deliver the subprotocol header by passing the token as the `protocols` argument to the `WebSocket` constructor:

```javascript
const ws = new WebSocket(
  `wss://api.platform.amigo.ai/v1/${WS}/sessions/connect` +
  `?service_id=${SVC}&entity_id=${ENTITY}`,
  ["auth", TOKEN],
);
```

{% endhint %}

{% hint style="warning" %}
The current TypeScript SDK URL helper exposes `conversationId` and assumes tool events are enabled when omitted. The server ignores `conversation_id` and defaults `tool_events` to `false`. Follow the server contract: omit `conversation_id` and append `tool_events=true` explicitly when tool telemetry is required. The SDK's `sessionConnectAuthProtocols()` helper does match the auth contract.
{% endhint %}

The endpoint does not publish a fixed Origin allowlist, connect-rate limit, concurrent-connection cap, or maximum connection lifetime as part of its application contract. Credentials authorize the connection. Keep credentials out of untrusted browser code, use one socket per active chat, and reconnect with capped exponential backoff and jitter.

#### Close codes

| Code   | Meaning                                                                                           |
| ------ | ------------------------------------------------------------------------------------------------- |
| `1000` | Normal close, including the close after an `idle_timeout` event                                   |
| `4000` | Invalid request parameters - workspace ID, `service_id`, or `entity_id` missing or malformed UUID |
| `4001` | Authentication failed - missing, invalid, inactive, expired, or wrong-workspace credentials       |
| `4004` | Resource not found - workspace missing or inactive, service not found, or person entity not found |
| `4503` | Conversation thread resolution timed out; retry with backoff                                      |

After a successful handshake, wait for `session.created`, then consume the automatic opening agent turn through its terminal `done` or `error` event. Only then send the first `user_text` message. A browser `open` event alone is not proof that validation succeeded because failures use immediate WebSocket close frames.

For frame shapes, server events, and close codes, see [Sessions](/developer-guide/platform-api/platform-api/sessions.md) and the [Sessions API reference](https://docs.amigo.ai/api-reference/readme/platform/sessions).

### Turn Sequencing

Send client messages as JSON text frames with `{"type":"user_text","text":"..."}`. The server processes one turn at a time. Wait for `done` or `error` before sending another message. A frame sent while the current turn is running can remain buffered by the WebSocket connection, but there is no public durable queue, replay cursor, or message receipt.

{% hint style="warning" %}
**Ambiguous disconnects.** If the socket closes before a turn reaches `done` or `error`, do not blindly resend the user message. Read the durable conversation using the most recent `conversation_id` to determine whether the turn was persisted.
{% endhint %}

### Tool Call Details

Pass `tool_events=true` on a Sessions WebSocket when the client needs streaming tool-call events. For REST turns and conversation detail, pass `include_tool_calls=true` to opt in to tool-call details, including failure details when available.

Tool-call output can include sensitive operational data, so it is opt-in rather than included by default.

### Greeting Behavior

| Situation                                 | What happens                                                                 |
| ----------------------------------------- | ---------------------------------------------------------------------------- |
| Every successful connection               | The server sends `session.created`, then runs an opening agent turn.         |
| Reconnect to an active thread             | The same `conversation_id` is returned, and another opening agent turn runs. |
| Connect after the prior thread was closed | A new conversation is created and receives its own opening turn.             |

## Outbound Text (SMS and iMessage)

To start an outbound SMS or iMessage conversation from your backend, use the create-conversation endpoint with an outbound `channel` (see [Starting an Outbound Conversation](#starting-an-outbound-conversation)). The platform resolves the sender from `use_case_id`, attempts the opening agent turn, and returns the durable conversation record. Triggers and other configured workflows can invoke related outbound paths. Recipient delivery remains a separate provider outcome, and an inbound reply resumes the active thread when routing succeeds.

### Inbound SMS and WhatsApp

Inbound SMS and WhatsApp deliveries enter through managed channel webhooks rather than a customer API call. The platform validates supported provider signatures, resolves the destination use case and service, normalizes the caller number, and serializes the message onto the current non-terminal thread or creates one when needed. Identity binding and available context depend on the configured channel, recognized caller, permissions, and projected data; an inbound number does not guarantee a complete patient match.

WhatsApp conversations have a longer default session window than SMS, reflecting the 24-hour messaging reply policy. Delivery status events (delivered, read, rejected) are tracked separately from message content and do not create conversation turns.

**SMS opt-out**: patients opt out by texting STOP, UNSUBSCRIBE, CANCEL, END, or QUIT to the agent's number. Outbound sends to opted-out numbers return 403.

***

## World Model Integration

When an entity is bound, a supported runtime can load selected authorized fields from the current [world model](/developer-guide/platform-api/data-world-model.md) projection. Missing upstream data and projection lag still apply. Clinical and platform tools are available only when the selected service, runtime, Context Graph, and authorization policy expose them; do not assume the voice tool catalog is universal to text.

***

## Signal Processing

Turn-producing signals run through the same high-level reasoning pipeline, although the available signals and delivery behavior depend on the channel:

```mermaid
flowchart TD
    signal["Signal arrives"]
    signal --> cut{"Turn boundary?"}
    cut -->|"message / surface submit / timeout"| yes["Yes"]
    cut -->|"delivery status / surface opened"| no["No-op"]
    yes --> nav["Navigate (reasoning engine, 60s timeout)"]
    nav --> respond["Send response"]
    nav --> complete["Terminal state - close"]
    nav --> tool["Tool call, then respond"]
```

| Signal                         | Turn?                     | What happens                                                                                              |
| ------------------------------ | ------------------------- | --------------------------------------------------------------------------------------------------------- |
| `message.inbound`              | Yes                       | Full reasoning engine pass                                                                                |
| `surface.submitted`            | Yes                       | Clears `wait_for` condition                                                                               |
| `review.approved`              | Yes                       | Clears `wait_for`                                                                                         |
| `timeout.idle` / `timeout.max` | Channel runtime dependent | Can end long-lived channel runtimes; this is separate from the Sessions WebSocket transport idle timeout. |
| `delivery.status`              | No                        | Logged only                                                                                               |
| `surface.opened`               | No                        | Logged only                                                                                               |

***

## Intelligence

Every conversation produces an intelligence record:

| Field               | Description              |
| ------------------- | ------------------------ |
| `quality_score`     | 0-100, penalty-based     |
| `turn_count`        | Total turns              |
| `completion_reason` | Why it ended             |
| `final_state`       | Last context graph state |

Workspace events: `text.started` (session created) and `text.completed` (session ended, with duration, turn count, reason, final state).

***

## Switching Between REST and WebSocket

The two transports share durable storage, but their start and resume inputs are not symmetric:

| Question                                                | Answer                                                                                                                                 |
| ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| Can REST inspect a WebSocket conversation?              | Yes. Use the `conversation_id` from `session.created`.                                                                                 |
| Can REST continue a WebSocket conversation?             | Yes. Wait for the WebSocket turn to finish, then send a REST turn to that conversation ID. Avoid driving both transports concurrently. |
| Can WebSocket select a REST-created conversation?       | No. `sessions/connect` does not accept a conversation ID; it resolves by workspace, entity, and service.                               |
| Does WebSocket reconnect preserve its own conversation? | Yes, while the thread remains active. Reconnect with the same entity and service. A terminal thread produces a new conversation.       |
| Do I need to re-authenticate?                           | Yes. Each connection or request authenticates independently.                                                                           |

## Graceful Degradation

| Failure                                    | What happens                                                                                                            |
| ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
| WebSocket validation fails                 | The connection opens and immediately closes with `4000`, `4001`, `4004`, or `4503`. No `session.created` frame is sent. |
| Opening or user turn fails downstream      | The server sends an `error` frame. The socket normally remains open for another turn.                                   |
| User turn exceeds 60 seconds               | The server sends `turn_timeout` with `retryable: true`; the socket remains open.                                        |
| No client frame arrives for 5 minutes      | The server sends `idle_timeout`, then closes the socket normally without closing the durable conversation.              |
| Socket closes before a terminal turn event | Outcome is ambiguous. Read conversation detail before retrying the user message.                                        |


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.amigo.ai/developer-guide/platform-api/conversations.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
