For the complete documentation index, see llms.txt. This page is also available as Markdown.

Conversations

Text-based agent conversations over REST and WebSocket - full CRUD lifecycle, persistent freeze-thaw, cross-channel continuity.

The conversations resource provides a unified view across all channels. Voice calls and text conversations are accessible through the same endpoints.

Conversations are first-class REST resources. You create them, send turns, list and inspect them, and close them when you are done. A separate WebSocket endpoint handles real-time streaming chat. Both run the same context graph engine, tools, and safety rules as voice calls, and both share the same conversation persistence - start over REST, resume over WebSocket, or the other way around.

One engine, many transports. REST, WebSocket, SMS, and WhatsApp all feed into the same actor. The agent does not know which transport delivered the message.

API Surface

Conversation resources cover create, list, detail, turn submission, and close operations. Sessions provide the public WebSocket surface for low-latency text conversations.

For exact paths, parameters, response schemas, SSE event shapes, and close codes, use the API reference:

Architecture


Conversation Lifecycle

State
What it means

Active

Engine running, processing a turn right now

Frozen

Turn finished, state saved. Ready for the next turn or WebSocket reconnect. At the wire level a quiesced conversation reports status active or completed with lifecycle dormant.

Closed

Terminal - agent finished or you explicitly closed it. No more turns accepted (409).

After every REST turn, the conversation is automatically frozen. The next POST /{id}/turns thaws it, processes the message, and freezes again. This is invisible to callers - you just keep sending turns.

Freeze/thaw is a lifecycle concept, not a wire value. At the wire level the status field is one of active, closed, completed, in-progress, or failed. The idle-vs-recent distinction described above is surfaced through a separate lifecycle field with values active, dormant, or closed. A quiesced ("frozen") conversation reports status active or completed together with lifecycle dormant.

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

Max duration

1 hour

Compress on freeze

Yes

Completion Reasons

When a conversation ends, the reason field tells you why:

Reason
What happened

completed

Agent reached a terminal context graph state

idle_timeout

No messages within the idle window (WebSocket)

max_duration

Hit the one-hour cap (WebSocket)

client_stop

Client sent {"type": "stop"} (WebSocket)

error

Unrecoverable processing error

transport_error

Three consecutive transport failures


REST API

Create a Conversation

Create or start a conversation (web inbound, or outbound on a channel)

post
Authorizations
AuthorizationstringRequired

API key issued via POST /v1/{workspace_id}/api-keys. Pass the returned api_key value as a Bearer token.

Path parameters
workspace_idstring · uuidRequired
Body
service_idstring · uuidRequired
entity_idstring · uuid · nullableOptional
channelstring · enumOptional

HSM execution channel type.

Determines how the HSM engine communicates with end users. Each kind maps to one or more providers.

Default: webPossible values:
recipientstring · min: 2 · max: 16 · nullableOptional

Destination address for an outbound conversation (E.164 for sms/imessage). Required for non-web.

use_case_idstring · uuid · nullableOptional

Channel-manager use case the outbound conversation is sent through. Required for non-web; channel-manager resolves the sender (FROM) from it (never caller-supplied). Must be owned by this workspace.

instructionstring · max: 10000 · nullableOptional

Optional context steering what the agent opens with on an outbound conversation.

force_newbooleanOptional

Outbound thread-keyed channels (sms/imessage) only: close any existing ACTIVE conversation on the computed provider thread (recipient + use case) before dispatching, so the opener materializes a brand-new conversation instead of continuing the old thread. Rejected with 422 on channel=web — every web create already starts a new conversation, so force_new is meaningless there.

Default: false
Responses
201

Successful Response

application/json
idstring · uuidRequired
channel_kindstring · enumRequired

HSM execution channel type.

Determines how the HSM engine communicates with end users. Each kind maps to one or more providers.

Possible values:
statusstring · enumRequiredPossible values:
lifecyclestring · enumRequiredPossible values:
entity_idstring · uuid · nullableOptional
service_idstring · uuid · nullableOptional
directionstring · nullableOptional
turn_countintegerOptionalDefault: 0
duration_secondsnumber · nullableOptional
created_atstringRequired
updated_atstringRequired
call_sidstring · nullableOptional
caller_idstring · nullableOptional
phone_numberstring · nullableOptional
quality_scorenumber · nullableOptional
has_recordingboolean · nullableOptional
completion_reasonstring · nullableOptional
escalation_statusstring · nullableOptional
final_statestring · nullableOptional
sourcestring · nullableOptional
planstring · nullableOptional
post/v1/{workspace_id}/conversations

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 endpoint defaults to an inbound web conversation. Set channel to an outbound channel to have the agent send the first message itself over that channel and return the durable conversation record.

Field
Type
Required
Description

service_id

uuid

Yes

Which agent.

entity_id

uuid

No

Patient entity for world model context.

channel

string

No

Defaults to web. Outbound-supported today: sms, imessage only.

recipient

string

For outbound

Recipient address (E.164 phone) on the outbound channel.

use_case_id

uuid

For outbound

Channel use case that resolves the sending number server-side. Must be owned by this workspace and service.

instruction

string

No

Steers the agent's opening message.

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

Send a message and get the agent's response

post

Send a user message and receive the agent's response. Set Accept: text/event-stream to receive an SSE stream of typed TurnStreamEvent frames (token, tool_call_started, tool_call_completed, thinking, message, done, error) instead of the synchronous JSON response. For new integrations prefer POST /turns/stream, which is always SSE.

Authorizations
AuthorizationstringRequired

API key issued via POST /v1/{workspace_id}/api-keys. Pass the returned api_key value as a Bearer token.

Path parameters
workspace_idstring · uuidRequired
conversation_idstring · uuidRequired
Query parameters
include_tool_callsbooleanOptional

Include tool call details in response

Default: false
pollbooleanOptional

Poll for background results without sending a user message. Drains any background tool calls that completed since the last turn and reports them; returns empty output when nothing is pending. Must NOT be combined with a request-body message (422) or SSE streaming (422). Poll no more than once every ~5s per conversation — each poll loads session state.

Default: false
Body
messagestring · max: 10000OptionalDefault: ""
media_urlstring · max: 2048 · nullableOptional
media_typestring · max: 128 · nullableOptional
contextstring · max: 100000 · nullableOptional

Injected into the agent's prompt as caller/patient context for this turn.

viewport_widthinteger · min: 20 · max: 500 · nullableOptional
viewport_heightinteger · min: 5 · max: 500 · nullableOptional
wait_for_finalboolean · nullableOptional

Synchronous callers only: when a turn hands work to a background tool (background_pending), 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). Ignored with poll=true (422).

suppress_fillerboolean · nullableOptional

Synchronous callers only: when a turn ends background_pending, omit the filler/acknowledgement text from output so a batch caller never mistakes the ack for the answer (output is empty; background_pending=true is the poll-later signal). null (default) inherits the channel policy. Ignored with poll=true (422).

Responses
200

Successful Response

turn_idstring · uuid · nullableRequired

Identifier of the user exchange this turn created — or, for poll=true and greeting-kickoff turns (empty message), the latest exchange the returned messages attach to. Deterministic: matches the turn_id on this conversation's history turns, so it can anchor durable per-turn artifacts (e.g. feedback) across page reloads. Null only when the conversation has no user exchange yet (a poll or kickoff before the first user message).

background_pendingbooleanOptional

Whether this turn's output is the final answer, or only an acknowledgement that work is still running in the background.

false (default): output is the complete agent response for this turn.

true: a tool crossed the server's blocking window and was handed to a background task, so output is NOT the final answer — the definitive assistant answer is produced later, out-of-band. It is durable on the conversation: drain it by re-issuing POST …/turns with poll=true (a no-message drain-and-report), by sending the next user turn, or by reading the conversation back (GET …/conversations/{id}). This is the per-conversation delivery contract — see the web-integration paved path. (The workspace observer bus, GET /v1/{workspace_id}/events/stream, mirrors this activity as text.agent_message / text.tool_started / text.background_result for dashboards watching many conversations, but is not the per-chat delivery path.) A client that treats a background_pending=true turn as complete without draining will miss the final answer.

Default: false
post/v1/{workspace_id}/conversations/{conversation_id}/turns

Send a message and get the agent's response. The conversation is thawed, the message runs through the reasoning engine, and the conversation is frozen again before the response returns.

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.

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

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 promptly with background_pending: true and an acknowledgement in output; the tool keeps running and the final answer is produced a few seconds later. The late answer is durable on the conversation - you never lose it - but you have to come back for it.

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

  • Send the next user turn. The completed background result is folded into that turn's reply automatically.

  • Poll. Re-issue POST /{id}/turns?poll=true with no message - a drain-and-report request that returns any results that have completed since the last turn (and background_pending: true again if more is still running). Keep polling until you get background_pending: false. A poll does not accept a message body, SSE streaming, or the per-turn wait/suppress controls (422).

  • Re-read the conversation. GET /{id} always reflects the durable, complete transcript. (This is why a late answer "appears after refresh" - the refresh just re-reads the conversation.)

A client that treats a background_pending: true turn as complete will silently miss the final answer. Poll (or send the next turn) until background_pending is false.

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.

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.


Streaming Turns (SSE)

For low-latency UIs that want to display tokens as they are generated and surface tool-call activity inline, use the dedicated streaming endpoint POST /v1/{workspace_id}/conversations/{conversation_id}/turns/stream, which always returns text/event-stream regardless of the Accept header. The same stream is also available on the regular turns endpoint by content negotiation:

The server keeps the connection open and emits a typed sequence of events (event: lines) terminated by a done event. Concurrency, lock semantics, and persistence behavior are identical to the JSON path - partial responses are persisted on client disconnect and the conversation freezes normally.

Event Schema

Every event carries an event discriminator that maps to a typed payload. SDK consumers using openapi-typescript (or any spec-driven generator) receive a discriminated union (TurnStreamEvent) for compile-time exhaustiveness.

Event
When
Payload

token

One agent response token

{ "text": string }

tool_call_started

Agent invoked a tool

{ "tool_name": string, "call_id": string, "input": string }

tool_call_completed

Tool returned

{ "tool_name": string, "call_id": string, "result": string, "succeeded": boolean }

thinking

Reasoning-tier classification

{ "tier": int, "tier_name": string }

message

Final assembled response

{ "role": string, "text": string }

done

Terminal event - turn complete

{ "conversation_id": string, "status": string, "turn_count": int }

error

Error mid-stream - connection ends

{ "message": string }

Example wire format

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, the server still finishes the turn and persists the partial agent response. The next GET /{id} returns the completed turn.

  • 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 turn while a WebSocket session owns the conversation

409 Conflict - "Conversation is already active".

Send a turn after the previous turn returned

Works normally - thaw, process, freeze.

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:

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

Retry strategy

Poll GET /v1/{workspace_id}/conversations/{id} and check lifecycle:

  • "active" - turn still processing. Wait and retry.

  • "dormant" - previous turn finished (status active or completed). Safe to send the next turn.

  • "closed" - conversation ended. Do not retry.

A simple approach: retry the turn with exponential backoff (1s, 2s, 4s) up to the 60-second turn timeout. Turns typically complete in 2-15 seconds depending on tool calls.

What if your HTTP client times out?

If your client gives up but the server is still processing, the turn runs to completion. The agent's response is generated, the conversation state is saved, and the conversation freezes normally. Your response is lost, but the state is not - the next GET /{id} will show the completed turn and the agent's reply in the turns array.

Lock release timing

The lock is released the instant the turn response is sent. If the server crashes mid-turn, the lock expires after ~180 seconds, after which new turns can be sent. There is no way to cancel an in-progress turn from the client side.


List Conversations

List all conversations (voice + text)

get
Authorizations
AuthorizationstringRequired

API key issued via POST /v1/{workspace_id}/api-keys. Pass the returned api_key value as a Bearer token.

Path parameters
workspace_idstring · uuidRequired
Query parameters
channel_kindstring · enum · nullableOptional

Filter by channel

Possible values:
statusstring · enum · nullableOptional

Filter by status

Possible values:
limitinteger · min: 1 · max: 100OptionalDefault: 20
offsetintegerOptionalDefault: 0
Responses
200

Successful Response

application/json
totalintegerRequired
has_morebooleanRequired
get/v1/{workspace_id}/conversations

The status filter accepts a wire status: active, closed, completed, in-progress, or failed. The active/dormant/closed view is exposed separately through the lifecycle field.

Turn count accuracy. The conversation list cross-references turn-level data when computing turn counts for text conversations. If the stored summary count is lower than the actual number of persisted turns, the response reflects the higher value.


Switch Conversation Channel

POST /v1/{workspace_id}/conversations/{conversation_id}/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.

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

Only sms and imessage are wired today; email, whatsapp, and voice return 501. Voice conversations cannot be switched. Re-keying preserves history and the compressed plan - only channel_kind, provider, and provider_thread_id change (provider is sendblue for imessage, otherwise twilio). An inbound reply on the new channel re-converges to the same conversation by construction: the routing key is derived from (channel, recipient, use_case_id) identically on both paths.

Request Body

Field
Type
Required
Description

channel

string

Yes

Target channel: sms or imessage.

recipient

string

Yes

Recipient address on the new channel (E.164 for SMS/iMessage).

use_case_id

uuid

Yes

Channel use case for the new channel (resolves the sender). Must be owned by this workspace.

reason

string

Yes

Why the conversation is being moved (e.g., escalation, customer_request). Non-empty, max 256 characters.

dispatch_opener

boolean

No

If true, immediately drive one agent turn on the new channel. Default false.

instruction

string

No

Optional context steering the opener when dispatch_opener is true. Max 10,000 characters.

Response

Returns the updated ConversationDetail with the new channel.

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

Get conversation detail (voice or text)

get
Authorizations
AuthorizationstringRequired

API key issued via POST /v1/{workspace_id}/api-keys. Pass the returned api_key value as a Bearer token.

Path parameters
workspace_idstring · uuidRequired
conversation_idstring · uuidRequired
Query parameters
include_tool_callsbooleanOptional

Include per-turn tool_calls[] in the returned turns. Off by default so the payload stays small and (potentially PHI-bearing) tool output is opt-in, matching POST /turns?include_tool_calls=true.

Default: false
Responses
200

Successful Response

application/json
idstring · uuidRequired
channel_kindstring · enumRequired

HSM execution channel type.

Determines how the HSM engine communicates with end users. Each kind maps to one or more providers.

Possible values:
statusstring · enumRequiredPossible values:
lifecyclestring · enumRequiredPossible values:
entity_idstring · uuid · nullableOptional
service_idstring · uuid · nullableOptional
directionstring · nullableOptional
turn_countintegerOptionalDefault: 0
duration_secondsnumber · nullableOptional
created_atstringRequired
updated_atstringRequired
call_sidstring · nullableOptional
caller_idstring · nullableOptional
phone_numberstring · nullableOptional
quality_scorenumber · nullableOptional
has_recordingboolean · nullableOptional
completion_reasonstring · nullableOptional
escalation_statusstring · nullableOptional
final_statestring · nullableOptional
sourcestring · nullableOptional
planstring · nullableOptional
get/v1/{workspace_id}/conversations/{conversation_id}

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

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.

Per-turn channel attribution. Each turn also carries a channel field (web, sms, imessage, …) in both GET /conversations and GET /conversations/{id}, recording which channel that turn was delivered on. After a channel switch, earlier turns keep their original channel and later turns reflect the new one. The field may be null on turns written before this feature shipped or on internal turns.


Close a Conversation

Close a conversation

delete
Authorizations
AuthorizationstringRequired

API key issued via POST /v1/{workspace_id}/api-keys. Pass the returned api_key value as a Bearer token.

Path parameters
workspace_idstring · uuidRequired
conversation_idstring · uuidRequired
Responses
204

Successful Response

No content

delete/v1/{workspace_id}/conversations/{conversation_id}

No content

Closes the conversation permanently. Subsequent turns return 409.


Example: Full REST Flow


WebSocket Sessions

Use Sessions for public bidirectional text conversations. Sessions share the conversation store and lifecycle with REST conversations: a turn started over WebSocket is visible to subsequent REST conversation detail calls.

The Sessions WebSocket resolves a stable conversation thread per (entity_id, service_id) pair. Clients reconnect with the same service_id and entity_id rather than passing a conversation ID.

Connect

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.

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.

Close codes

Code
Meaning

1000

Normal close

4000

Invalid request parameters - workspace ID, service_id, or entity_id missing or malformed UUID

4001

Authentication failed - missing, invalid, or expired credentials

4004

Resource not found - workspace inactive, service not found, or entity not found

4503

Service unavailable - session creation timed out; retry with backoff

The server enforces a one-hour cap on conversation duration and a 5-minute idle timeout (no inbound frames). Reconnect with the same service_id + entity_id to continue the session.

For frame shapes, server events, and close codes, see Sessions and the Sessions API reference.

Message Queuing and Coalescing

Messages sent while the agent is still processing a previous message are queued and processed in order. There is no barge-in - the agent finishes its current response before starting the next one.

SMS and WhatsApp coalesce rapid-fire messages automatically. When multiple messages arrive before the agent starts its next turn, they are joined into a single turn (newline-separated) and the agent responds once to all of them. This prevents awkward split responses when a patient sends "Hi" then "I need to schedule an appointment" in quick succession - the agent sees both as one message and responds coherently.

WebSocket does not coalesce by default. Each message produces its own typingmessage response cycle. If you send 3 messages while the agent is busy, you will receive 3 separate responses, in order.

Scenario
What happens

Send a message while agent is responding

Queued. Processed after the current response completes.

Send multiple messages rapidly (SMS/WhatsApp)

Coalesced into one turn. One combined response.

Send multiple messages rapidly (WebSocket)

Each processed as its own turn, in order. One response per message.

Send a message during a tool call

Queued. Processed after the tool call and response complete.

Agent reaches terminal state while messages are queued

Session ends. Queued messages are discarded. You receive session_ended.

Disconnect while messages are queued

Unprocessed messages are discarded. The conversation freezes with whatever turns completed.

This is different from voice calls, where the patient's speech interrupts (barge-in) the agent. In text mode, every message waits its turn.

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

New conversation

Agent generates and sends a greeting

Resumed (thawed)

No greeting - agent waits for you to speak

Outbound Text (SMS and iMessage)

Agent-initiated outbound text over SMS and iMessage is dispatched by platform internals - triggers, connector-driven workflows, and scheduled outreach - rather than by a public API call. To start an outbound text conversation from your own backend, use the create-conversation endpoint with an outbound channel (see Starting an Outbound Conversation). The platform resolves the sending number server-side from the use_case_id, sends the agent's first message, and returns the durable conversation record. An inbound reply from the recipient resumes the same conversation.

Inbound SMS and WhatsApp

Inbound conversations are automatic - no API call is needed. When a patient texts the agent's SMS or WhatsApp number, the platform validates the inbound message signature, resolves the patient's phone number to a workspace and service, and routes the message to the active conversation for that phone pair, creating one with full patient context from the world model if none exists. Phone numbers are normalized to E.164 format, so international numbers are handled correctly regardless of how the messaging provider formats them.

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 entity_id is provided, the agent starts with the patient's full world model projection: demographics, medications, allergies, conditions, appointments, insurance. The same clinical tools available during voice calls work here.


Signal Processing

Every turn goes through the same three-step pipeline:

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

Yes

Session ends

delivery.status

No

Logged only

surface.opened

No

Logged only

Transport failures on one turn do not kill the session. Three consecutive failures do.


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

You can start a conversation over REST and resume it over WebSocket, or vice versa. The conversation ID is the same across both transports. A few things to know:

Question
Answer

Can I switch mid-conversation?

Yes. Close the WebSocket (or let the REST turn complete), then use the other transport with the same conversation_id.

How quickly after a WebSocket disconnect can I send a REST turn?

Immediately in most cases. The lock is released during cleanup. If the server crashed, wait up to ~180 seconds for the lock to expire.

Do I need to re-authenticate?

Yes. Each connection or request authenticates independently.

Is the conversation state the same?

Yes. Both transports read from and write to the same conversation store. Turns from one transport are visible to the other.

Graceful Degradation

Failure
What happens

One transport send fails

Logged, session continues

Three consecutive failures

Session ends (transport_error)

Navigation timeout (>60s)

Turn skipped, session continues

State load fails

Fresh start, state not saved on end

Compression fails on freeze

Saved without plan - raw turns preserved

Engine init fails

WS: close 4503. REST: 503.

Server crashes mid-turn

Conversation freezes with last saved state. Lock expires in ~180s. Resume with next turn or reconnect.

Last updated

Was this helpful?