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

Conversations

Create, inspect, continue, switch, and close durable agent conversations over REST or WebSocket while preserving cross-channel history and state.

Conversation detail is channel-aware, so voice calls and text conversations can be inspected through the same resource shape. Use the unified Runs endpoint to browse activity across channels.

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.

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.

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.

The REST operations are embedded in the relevant sections below. The Sessions guide covers the separate WebSocket contract and close codes.

Architecture


Conversation Lifecycle

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.

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.

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

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
truncatedbooleanOptional

True when the conversation recorded more entries than one read takes (10,000), so the turns stop before the end of the conversation.

Default: false
planstring · nullableOptional
post/v1/{workspace_id}/conversations
POST /v1/{workspace_id}/conversations HTTP/1.1
Host: platform.api-us.concurrence.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 271

{
  "service_id": "123e4567-e89b-12d3-a456-426614174000",
  "entity_id": "123e4567-e89b-12d3-a456-426614174000",
  "channel": "web",
  "recipient": "text",
  "use_case_id": "123e4567-e89b-12d3-a456-426614174000",
  "instruction": "text",
  "force_new": false,
  "context": {
    "ANY_ADDITIONAL_PROPERTY": 1
  }
}
{
  "id": "123e4567-e89b-12d3-a456-426614174000",
  "channel_kind": "voice",
  "status": "active",
  "lifecycle": "active",
  "entity_id": "123e4567-e89b-12d3-a456-426614174000",
  "service_id": "123e4567-e89b-12d3-a456-426614174000",
  "direction": "text",
  "turn_count": 0,
  "duration_seconds": 1,
  "created_at": "text",
  "updated_at": "text",
  "call_sid": "text",
  "caller_id": "text",
  "phone_number": "text",
  "quality_score": 1,
  "has_recording": true,
  "completion_reason": "text",
  "escalation_status": "text",
  "final_state": "text",
  "source": "text",
  "turns": [
    {
      "turn_id": "123e4567-e89b-12d3-a456-426614174000",
      "turn_index": 1,
      "role": "agent",
      "text": "text",
      "timestamp": "text",
      "channel": "voice",
      "content": [
        {
          "type": "text",
          "text": "text",
          "url": "text",
          "media_type": "text",
          "provider_id": "text",
          "metadata": {
            "ANY_ADDITIONAL_PROPERTY": "anything"
          }
        }
      ],
      "tool_calls": [
        {
          "tool_name": "text",
          "call_id": "text",
          "input": {
            "ANY_ADDITIONAL_PROPERTY": "anything"
          },
          "result": "",
          "succeeded": true,
          "duration_ms": 1,
          "error_message": "text"
        }
      ],
      "artifacts": [
        {
          "type": "text",
          "schema_version": "text",
          "payload": "[Circular Reference]"
        }
      ],
      "selected_action_description": "text",
      "state_transitions": [
        {
          "from": "text",
          "to": "text"
        }
      ],
      "available_actions": [
        {
          "description": "text"
        }
      ]
    }
  ],
  "truncated": false,
  "plan": "text",
  "voice": {
    "recording_path": "text",
    "twilio_recording_sid": "text",
    "twilio_recording_duration": 1,
    "has_recording": false,
    "emotional_summary": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    },
    "barge_in_events": [
      {
        "ANY_ADDITIONAL_PROPERTY": "anything"
      }
    ],
    "states_visited": [
      "text"
    ],
    "participants": [
      {
        "ANY_ADDITIONAL_PROPERTY": "anything"
      }
    ],
    "config": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    },
    "call_analysis": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    },
    "forwarding": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    },
    "escalation": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    },
    "safety": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    },
    "quality_breakdown": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    },
    "risk_summary": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    },
    "latency_summary": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    },
    "conversation_metrics": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    },
    "tool_summary": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    },
    "safety_summary": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    },
    "operator_summary": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    }
  }
}

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

The optional start-time context map supplies string and number values declared in the owning service's accepted_context. Creation validates and stores the map for tool bindings in the selected Context Graph. It is separate from an individual turn's free-text context field. A key the service does not declare, a missing required key, or a value of the wrong type returns 422 with the key in loc, for example ["body", "context", "plan_id"]. Validation errors never echo the submitted value. See Pass Context to Tool Parameters for request examples, binding setup, and automatic channel-identity limitations.

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 in this workspace, or use case not bound to this workspace and 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 a background result without sending a user message. Protocol v2 claims at most one ready answer and returns an opaque delivery receipt; render or durably persist the answer, then acknowledge that receipt. Returns empty output when nothing is ready. Must NOT be combined with a request-body message (422) or SSE streaming (422). Poll no more than once every ~5s per conversation.

Default: false
Header parameters
Idempotency-Keystring · uuid · nullableOptional

Stable UUID for replaying the same logical send or poll after an ambiguous failure. Reuse the same value only for that operation. Omission is backward-compatible but cannot provide client-controlled response-loss replay.

Body
messagestring · max: 10000Optional

The user's message. Leave it empty (with no content or media) only to kick off a conversation that has no turns yet: the agent answers with its greeting. Once the conversation has a turn, a turn with no user input is a 422.

Default: ""
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 exchange the returned messages attach to. A background delivery keeps the originating exchange even when later user turns already exist. 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. Under delivery protocol v2, collect it by re-issuing POST …/turns with poll=true (a no-message, receipt-backed claim), render or durably persist the returned answer, then acknowledge its exact delivery receipt. This is the per-conversation delivery contract — see the web-integration paved path. (GET /v1/{workspace_id}/events/stream?conversation_id= mirrors this activity as text.tool_started / text.background_result / text.agent_message for a view watching this one conversation, but is not the per-chat delivery path. The stream without conversation_id carries none of it.) A client that treats a background_pending=true turn as complete without polling and acknowledging will miss the final answer.

Default: false
delivery_protocol_versionconst: 2 · nullableOptional

Version of the durable background-delivery protocol supported by the serving agent. Clients may retry polls only after observing version 2.

post/v1/{workspace_id}/conversations/{conversation_id}/turns
POST /v1/{workspace_id}/conversations/{conversation_id}/turns HTTP/1.1
Host: platform.api-us.concurrence.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 300

{
  "message": "",
  "content": [
    {
      "type": "text",
      "text": "text",
      "url": "text",
      "media_type": "text",
      "provider_id": "text",
      "metadata": {
        "ANY_ADDITIONAL_PROPERTY": "anything"
      }
    }
  ],
  "media_url": "text",
  "media_type": "text",
  "context": "text",
  "viewport_width": 1,
  "viewport_height": 1,
  "wait_for_final": true,
  "suppress_filler": true
}
{
  "turn_id": "123e4567-e89b-12d3-a456-426614174000",
  "input": {
    "turn_id": "123e4567-e89b-12d3-a456-426614174000",
    "turn_index": 1,
    "role": "agent",
    "text": "text",
    "timestamp": "text",
    "channel": "voice",
    "content": [
      {
        "type": "text",
        "text": "text",
        "url": "text",
        "media_type": "text",
        "provider_id": "text",
        "metadata": {
          "ANY_ADDITIONAL_PROPERTY": "anything"
        }
      }
    ],
    "tool_calls": [
      {
        "tool_name": "text",
        "call_id": "text",
        "input": {
          "ANY_ADDITIONAL_PROPERTY": "anything"
        },
        "result": "",
        "succeeded": true,
        "duration_ms": 1,
        "error_message": "text"
      }
    ],
    "artifacts": [
      {
        "type": "text",
        "schema_version": "text",
        "payload": {
          "ANY_ADDITIONAL_PROPERTY": "[Circular Reference]"
        }
      }
    ],
    "selected_action_description": "text",
    "state_transitions": [
      {
        "from": "text",
        "to": "text"
      }
    ],
    "available_actions": [
      {
        "description": "text"
      }
    ]
  },
  "output": [
    {
      "turn_id": "123e4567-e89b-12d3-a456-426614174000",
      "turn_index": 1,
      "role": "agent",
      "text": "text",
      "timestamp": "text",
      "channel": "voice",
      "content": [
        {
          "type": "text",
          "text": "text",
          "url": "text",
          "media_type": "text",
          "provider_id": "text",
          "metadata": {
            "ANY_ADDITIONAL_PROPERTY": "anything"
          }
        }
      ],
      "tool_calls": [
        {
          "tool_name": "text",
          "call_id": "text",
          "input": {
            "ANY_ADDITIONAL_PROPERTY": "anything"
          },
          "result": "",
          "succeeded": true,
          "duration_ms": 1,
          "error_message": "text"
        }
      ],
      "artifacts": [
        {
          "type": "text",
          "schema_version": "text",
          "payload": {
            "ANY_ADDITIONAL_PROPERTY": "[Circular Reference]"
          }
        }
      ],
      "selected_action_description": "text",
      "state_transitions": [
        {
          "from": "text",
          "to": "text"
        }
      ],
      "available_actions": [
        {
          "description": "text"
        }
      ]
    }
  ],
  "tool_calls": [
    {
      "tool_name": "text",
      "call_id": "text",
      "input": {
        "ANY_ADDITIONAL_PROPERTY": "anything"
      },
      "result": "",
      "succeeded": true,
      "duration_ms": 1,
      "error_message": "text"
    }
  ],
  "artifacts": [
    {
      "type": "text",
      "schema_version": "text",
      "payload": {
        "ANY_ADDITIONAL_PROPERTY": "[Circular Reference]"
      }
    }
  ],
  "conversation": {
    "id": "123e4567-e89b-12d3-a456-426614174000",
    "status": "active",
    "turn_count": 0,
    "updated_at": "text",
    "context_graph_state": {
      "type": "text",
      "name": "text",
      "objective": "text",
      "actions": [
        {
          "description": "text",
          "filler_hint": "text"
        }
      ],
      "intra_state_navigation_guidelines": [
        "text"
      ],
      "action_guidelines": [
        "text"
      ],
      "boundary_constraints": [
        "text"
      ],
      "exit_conditions": [
        {
          "description": "text",
          "next_state": "text",
          "filler_hint": "text"
        }
      ],
      "action_tool_call_specs": [
        {
          "tool_id": "text",
          "additional_instruction": "",
          "navigate_on_completion": false,
          "progress": {
            "deterministic": false,
            "phrases": [
              "text"
            ],
            "trigger_delay_ms": 1,
            "interval_ms": 1,
            "mode": "auto",
            "progress_class": "lookup",
            "expected_latency_ms": 1,
            "custom_phrase": "text"
          },
          "result_persistence": "accumulate",
          "delivery": "interrupt",
          "failure_delivery": "interrupt",
          "failure_delivery_by_class": {
            "ANY_ADDITIONAL_PROPERTY": "interrupt"
          },
          "execution": "blocking",
          "lifecycle": "independent",
          "context_bindings": [
            "text"
          ],
          "context_binding_aliases": {
            "ANY_ADDITIONAL_PROPERTY": "text"
          },
          "audio_fillers": [
            "text"
          ],
          "audio_filler_triggered_after": 1
        }
      ],
      "exit_condition_tool_call_specs": [
        {
          "tool_id": "text",
          "additional_instruction": "",
          "navigate_on_completion": false,
          "progress": {
            "deterministic": false,
            "phrases": [
              "text"
            ],
            "trigger_delay_ms": 1,
            "interval_ms": 1,
            "mode": "auto",
            "progress_class": "lookup",
            "expected_latency_ms": 1,
            "custom_phrase": "text"
          },
          "result_persistence": "accumulate",
          "delivery": "interrupt",
          "failure_delivery": "interrupt",
          "failure_delivery_by_class": {
            "ANY_ADDITIONAL_PROPERTY": "interrupt"
          },
          "execution": "blocking",
          "lifecycle": "independent",
          "context_bindings": [
            "text"
          ],
          "context_binding_aliases": {
            "ANY_ADDITIONAL_PROPERTY": "text"
          },
          "audio_fillers": [
            "text"
          ],
          "audio_filler_triggered_after": 1
        }
      ],
      "wait_for": "surface_submission",
      "guardrails": [
        {
          "name": "text",
          "description": "text",
          "enforcement": "hard"
        }
      ],
      "channel_overrides": {
        "ANY_ADDITIONAL_PROPERTY": {
          "objective": "text",
          "action_guidelines": [
            "text"
          ],
          "progress": {
            "deterministic": false,
            "phrases": [
              "text"
            ],
            "trigger_delay_ms": 1,
            "interval_ms": 1,
            "mode": "auto",
            "progress_class": "lookup",
            "expected_latency_ms": 1,
            "custom_phrase": "text"
          }
        }
      },
      "surface_spec_template": {
        "ANY_ADDITIONAL_PROPERTY": "anything"
      },
      "turn_policy": {
        "barge_in_enabled": true,
        "safety_response": "stay_empathize",
        "context_strategy": "full",
        "degradation_threshold": -1,
        "block_forward_call": false,
        "block_forward_call_after_turns": -1,
        "stt_eot_threshold": 1,
        "stt_eager_eot_threshold": 1,
        "stt_eot_timeout_ms": 1
      },
      "preload": [
        {
          "tool_id": "text",
          "params": {
            "ANY_ADDITIONAL_PROPERTY": "anything"
          }
        }
      ]
    }
  },
  "background_pending": false,
  "delivery": {
    "delivery_id": "123e4567-e89b-12d3-a456-426614174000",
    "request_id": "123e4567-e89b-12d3-a456-426614174000",
    "receipt": "123e4567-e89b-12d3-a456-426614174000"
  },
  "delivery_protocol_version": 1
}

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.

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. The snapshot's context_graph_state is the state after the turn, and the output carries the turn's state_transitions. A 409 is returned when the conversation is closed or another turn is already being processed (see Concurrency below).

Empty turns. An empty message with no content or media is accepted only as the kickoff of a conversation that has no turns yet, and the agent answers with its greeting. Once the conversation has a turn, a turn with no user input returns 422 with loc ["body", "message"], on both POST .../turns and POST .../turns/stream.

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). When two turns on one conversation overlap and both are accepted, each response carries its own turn identity.

Model rejections. When the model rejects a turn with a 4xx, the turn fails at once with 502 and "Model rejected the request (HTTP <status>)"; any other failure inside the turn returns 500 "Agent response failed". Neither includes the upstream response text. A retry with the same key replays the failure, so fix the rejected configuration or content and send a new turn.


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.

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

Send a message and receive a streamed agent response

post

Streaming variant of POST /turns. Always returns text/event-stream regardless of the Accept header — no JSON fallback. Each frame is a TurnStreamEvent discriminated by the event field (token, tool_call_started, tool_call_completed, thinking, message, done, error). Use this endpoint for new integrations; the Accept-sniffing variant remains for backward compatibility.

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_started / tool_call_completed frames in the stream

Default: false
Header parameters
Idempotency-Keystring · uuid · nullableOptional

Stable UUID for replaying the same logical streamed user-input turn.

Body
messagestring · max: 10000Optional

The user's message. Leave it empty (with no content or media) only to kick off a conversation that has no turns yet: the agent answers with its greeting. Once the conversation has a turn, a turn with no user input is a 422.

Default: ""
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

SSE stream of TurnStreamEvent frames

anyOptional
post/v1/{workspace_id}/conversations/{conversation_id}/turns/stream
POST /v1/{workspace_id}/conversations/{conversation_id}/turns/stream HTTP/1.1
Host: platform.api-us.concurrence.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 300

{
  "message": "",
  "content": [
    {
      "type": "text",
      "text": "text",
      "url": "text",
      "media_type": "text",
      "provider_id": "text",
      "metadata": {
        "ANY_ADDITIONAL_PROPERTY": "anything"
      }
    }
  ],
  "media_url": "text",
  "media_type": "text",
  "context": "text",
  "viewport_width": 1,
  "viewport_height": 1,
  "wait_for_final": true,
  "suppress_filler": true
}

No content

Example using the regular turns operation:

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

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 a progressive preview of the reply. When message arrives, replace the preview with the message text instead of appending it, and commit only the message text to the transcript.

  • Tool-backed turns can emit tool_call_started and tool_call_completed before the final message, but those events can arrive after the turn's token events. Do not rely on the order of token and tool-call events.

  • Tool calls can run before the first token arrives. Show a pending indicator until the first token or message event, and keep the response open until done.

  • Every 15 seconds the stream carries a comment line that starts with : (: ping - <timestamp>), so proxies and load balancers do not close a turn that is waiting on a slow tool. A comment is not an event and has no data: line. Standard SSE parsers skip it, and a hand-written parser must skip it too.

  • A mid-stream error event is terminal - no done follows. Branch on code and retryable, and retry with backoff only when retryable is true.

  • Check retryable before retrying. If the conversation is closed while a streamed turn is running, for example by a close from another client, the stream ends with this terminal frame:

    It is the in-stream form of the 409 Conversation is closed returned before a stream starts. Treat it as terminal: stop the turn, do not retry, and start a new conversation if the user continues. A close that lands mid-turn is reported on this error frame rather than leaving any already-streamed token preview standing as the answer.

  • A streamed turn that times out without a reply ends with a retryable error frame (code: upstream_error, retryable: true, status_code: 504) because the turn may still be running; a retry can still receive its reply. A turn that ended but produced no reply ends with a non-retryable error frame (code: upstream_error, retryable: false, status_code: 500): a same-key retry replays the ended turn rather than re-running it, so send a new turn instead.

  • When the model rejects a streamed turn, the stream ends at once with a non-retryable error frame (code: upstream_error, retryable: false, status_code: 502) whose message is Model rejected the request (HTTP <status>). Do not retry it with the same key; fix the rejected configuration or content first.

  • When a streamed turn relayed token events but the final reply carries no agent message, the stream still sends a message event with empty text before done, so the completed empty message replaces the token preview rather than leaving the preview as the answer. A turn that relayed no tokens and produced no message sends no message event before done.

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

Close the conversation while a streamed turn is running

The stream ends with a non-retryable error frame, "Conversation is closed" with status_code 409. See Streaming behavior.

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.

A conversation closed while a JSON (non-streaming) turn is running returns the same 409 "Conversation is closed" for that turn. The request ends shortly after the close instead of waiting for the reply deadline.

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.


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.

List runs (framework + conversation)

get

Paginated, newest-first list of runs for the workspace behind the unified Run contract. Federates framework runs (Delta world.runs MV) and conversation runs (Lakebase world.conversations) at read time. Filter by kind (conversation / framework), channel (voice/sms/whatsapp/email/web/imessage — conversation runs only), status (live expands to running + paused), service_id (service UUIDs), and search (entity name, caller ID or phone number). Repeated filter values are ORed within each axis; axes combine with AND. Service filtering applies to conversation and live voice runs before pagination. Historical framework runs have no stored service attribution and are excluded when service_id is set. continuation_token is an opaque page cursor.

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
sort_bystring[]OptionalDefault: []
limitinteger · max: 200OptionalDefault: 50
continuation_tokenanyOptional
service_idstring · uuid[] · max: 100Optional

Filter conversation and live voice runs by up to 100 service IDs. Historical framework runs have no stored service attribution and are excluded. Repeated values are ORed; combines with other filters using AND.

Default: []
searchstring · min: 1 · max: 200 · nullableOptional

Case-insensitive substring match on a conversation run's entity name, caller ID or phone number, applied before pagination. Framework runs carry none of these and are excluded when search is set. Combines with other filters using AND.

Responses
200

Successful Response

application/json
has_morebooleanRequired
continuation_tokenanyOptional
get/v1/{workspace_id}/runs
GET /v1/{workspace_id}/runs HTTP/1.1
Host: platform.api-us.concurrence.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
  "items": [
    {
      "run_id": "123e4567-e89b-12d3-a456-426614174000",
      "workspace_id": "123e4567-e89b-12d3-a456-426614174000",
      "kind": "conversation",
      "status": "running",
      "channel": "voice",
      "framework": "claude-agent-sdk",
      "entity_id": "123e4567-e89b-12d3-a456-426614174000",
      "service_id": "123e4567-e89b-12d3-a456-426614174000",
      "started_at": "2026-01-01T00:00:00.000Z",
      "ended_at": "2026-01-01T00:00:00.000Z",
      "entity_name": "text",
      "service_name": "text",
      "caller_id": "text",
      "phone_number": "text",
      "direction": "text",
      "turn_count": 1,
      "completion_reason": "text",
      "source_call_sid": "text",
      "source_conversation_id": "123e4567-e89b-12d3-a456-426614174000",
      "source_framework_run_id": "123e4567-e89b-12d3-a456-426614174000",
      "takeover": {
        "eligible": true,
        "mode_options": [
          "listen"
        ],
        "reason": "text"
      }
    }
  ],
  "has_more": true,
  "continuation_token": null
}

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.

Switch a conversation to another channel

post

Move an active conversation onto a different channel (sms/imessage today). The durable conversation id is preserved — the same conversation continues; only its routing changes. Optionally dispatch a first agent turn on the new channel.

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
Body
channelstring · enumRequired

Target channel to move the conversation to (sms or imessage).

Possible values:
recipientstring · min: 2 · max: 16 · nullableOptional

Recipient address on the new channel (E.164). Required for sms/imessage.

use_case_idstring · uuid · nullableOptional

Channel-manager use case for the new channel (resolves the sender). Required for sms/imessage.

reasonstring · min: 1 · max: 256Required

Why the conversation is being moved (e.g. escalation, customer_request).

dispatch_openerbooleanOptional

If true, immediately drive one agent turn on the new channel (e.g. 'I'll text you now').

Default: false
instructionstring · max: 10000 · nullableOptional

Optional context steering the opener when dispatch_opener=true.

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
truncatedbooleanOptional

True when the conversation recorded more entries than one read takes (10,000), so the turns stop before the end of the conversation.

Default: false
planstring · nullableOptional
post/v1/{workspace_id}/conversations/{conversation_id}/channel
POST /v1/{workspace_id}/conversations/{conversation_id}/channel HTTP/1.1
Host: platform.api-us.concurrence.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 152

{
  "channel": "voice",
  "recipient": "text",
  "use_case_id": "123e4567-e89b-12d3-a456-426614174000",
  "reason": "text",
  "dispatch_opener": false,
  "instruction": "text"
}
{
  "id": "123e4567-e89b-12d3-a456-426614174000",
  "channel_kind": "voice",
  "status": "active",
  "lifecycle": "active",
  "entity_id": "123e4567-e89b-12d3-a456-426614174000",
  "service_id": "123e4567-e89b-12d3-a456-426614174000",
  "direction": "text",
  "turn_count": 0,
  "duration_seconds": 1,
  "created_at": "text",
  "updated_at": "text",
  "call_sid": "text",
  "caller_id": "text",
  "phone_number": "text",
  "quality_score": 1,
  "has_recording": true,
  "completion_reason": "text",
  "escalation_status": "text",
  "final_state": "text",
  "source": "text",
  "turns": [
    {
      "turn_id": "123e4567-e89b-12d3-a456-426614174000",
      "turn_index": 1,
      "role": "agent",
      "text": "text",
      "timestamp": "text",
      "channel": "voice",
      "content": [
        {
          "type": "text",
          "text": "text",
          "url": "text",
          "media_type": "text",
          "provider_id": "text",
          "metadata": {
            "ANY_ADDITIONAL_PROPERTY": "anything"
          }
        }
      ],
      "tool_calls": [
        {
          "tool_name": "text",
          "call_id": "text",
          "input": {
            "ANY_ADDITIONAL_PROPERTY": "anything"
          },
          "result": "",
          "succeeded": true,
          "duration_ms": 1,
          "error_message": "text"
        }
      ],
      "artifacts": [
        {
          "type": "text",
          "schema_version": "text",
          "payload": "[Circular Reference]"
        }
      ],
      "selected_action_description": "text",
      "state_transitions": [
        {
          "from": "text",
          "to": "text"
        }
      ],
      "available_actions": [
        {
          "description": "text"
        }
      ]
    }
  ],
  "truncated": false,
  "plan": "text",
  "voice": {
    "recording_path": "text",
    "twilio_recording_sid": "text",
    "twilio_recording_duration": 1,
    "has_recording": false,
    "emotional_summary": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    },
    "barge_in_events": [
      {
        "ANY_ADDITIONAL_PROPERTY": "anything"
      }
    ],
    "states_visited": [
      "text"
    ],
    "participants": [
      {
        "ANY_ADDITIONAL_PROPERTY": "anything"
      }
    ],
    "config": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    },
    "call_analysis": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    },
    "forwarding": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    },
    "escalation": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    },
    "safety": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    },
    "quality_breakdown": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    },
    "risk_summary": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    },
    "latency_summary": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    },
    "conversation_metrics": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    },
    "tool_summary": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    },
    "safety_summary": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    },
    "operator_summary": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    }
  }
}

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

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
truncatedbooleanOptional

True when the conversation recorded more entries than one read takes (10,000), so the turns stop before the end of the conversation.

Default: false
planstring · nullableOptional
get/v1/{workspace_id}/conversations/{conversation_id}
GET /v1/{workspace_id}/conversations/{conversation_id} HTTP/1.1
Host: platform.api-us.concurrence.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
  "id": "123e4567-e89b-12d3-a456-426614174000",
  "channel_kind": "voice",
  "status": "active",
  "lifecycle": "active",
  "entity_id": "123e4567-e89b-12d3-a456-426614174000",
  "service_id": "123e4567-e89b-12d3-a456-426614174000",
  "direction": "text",
  "turn_count": 0,
  "duration_seconds": 1,
  "created_at": "text",
  "updated_at": "text",
  "call_sid": "text",
  "caller_id": "text",
  "phone_number": "text",
  "quality_score": 1,
  "has_recording": true,
  "completion_reason": "text",
  "escalation_status": "text",
  "final_state": "text",
  "source": "text",
  "turns": [
    {
      "turn_id": "123e4567-e89b-12d3-a456-426614174000",
      "turn_index": 1,
      "role": "agent",
      "text": "text",
      "timestamp": "text",
      "channel": "voice",
      "content": [
        {
          "type": "text",
          "text": "text",
          "url": "text",
          "media_type": "text",
          "provider_id": "text",
          "metadata": {
            "ANY_ADDITIONAL_PROPERTY": "anything"
          }
        }
      ],
      "tool_calls": [
        {
          "tool_name": "text",
          "call_id": "text",
          "input": {
            "ANY_ADDITIONAL_PROPERTY": "anything"
          },
          "result": "",
          "succeeded": true,
          "duration_ms": 1,
          "error_message": "text"
        }
      ],
      "artifacts": [
        {
          "type": "text",
          "schema_version": "text",
          "payload": "[Circular Reference]"
        }
      ],
      "selected_action_description": "text",
      "state_transitions": [
        {
          "from": "text",
          "to": "text"
        }
      ],
      "available_actions": [
        {
          "description": "text"
        }
      ]
    }
  ],
  "truncated": false,
  "plan": "text",
  "voice": {
    "recording_path": "text",
    "twilio_recording_sid": "text",
    "twilio_recording_duration": 1,
    "has_recording": false,
    "emotional_summary": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    },
    "barge_in_events": [
      {
        "ANY_ADDITIONAL_PROPERTY": "anything"
      }
    ],
    "states_visited": [
      "text"
    ],
    "participants": [
      {
        "ANY_ADDITIONAL_PROPERTY": "anything"
      }
    ],
    "config": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    },
    "call_analysis": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    },
    "forwarding": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    },
    "escalation": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    },
    "safety": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    },
    "quality_breakdown": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    },
    "risk_summary": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    },
    "latency_summary": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    },
    "conversation_metrics": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    },
    "tool_summary": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    },
    "safety_summary": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    },
    "operator_summary": {
      "ANY_ADDITIONAL_PROPERTY": "anything"
    }
  }
}

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

Each voice utterance appears once. With include_tool_calls=true, each tool call appears on the agent turn that made it. truncated is true when the conversation recorded more entries than one read takes, so the turns stop before the end of the conversation.

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 carries a channel field (web, sms, imessage, and others), recording which channel delivered it. After a channel switch, earlier turns keep their original channel and later turns reflect the new one. The field may be null on older or internal turns.

List Conversation Turns

Pages through the same turns the detail returns, for voice or text. The default sort_by of +turn_index starts with any messages before the first user turn, and -turn_index starts with the newest exchange. Messages inside one exchange keep conversation order either way. limit accepts up to 200, and continuation_token is opaque: pass the returned value unchanged. Pass include_tool_calls=true to include tool calls, and check truncated as on the detail.

List conversation turns (voice or text)

get

Page through the turns GET /conversations/{conversation_id} returns, ordered by exchange (turn_index). The default +turn_index starts with the messages before the first user turn; -turn_index starts with the newest exchange and ends with them. Messages inside one exchange keep conversation order either way. Turns are folded from the conversation's journal on each request rather than read from a table, so truncated matches the detail's flag. When it is true, the turns stop at the read bound, before the end of the conversation, in either sort order.

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
sort_bystring[]OptionalDefault: []
limitinteger · max: 200OptionalDefault: 50
continuation_tokenanyOptional
include_tool_callsbooleanOptional

Include per-turn tool_calls[] in the returned turns. Off by default, matching GET /conversations/{conversation_id}?include_tool_calls=true.

Default: false
Responses
200

Successful Response

application/json
has_morebooleanRequired
continuation_tokenanyOptional
truncatedbooleanRequired

True when the conversation recorded more entries than one read takes (10,000), so the turns stop before the end of the conversation.

get/v1/{workspace_id}/conversations/{conversation_id}/turns
GET /v1/{workspace_id}/conversations/{conversation_id}/turns HTTP/1.1
Host: platform.api-us.concurrence.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
  "items": [
    {
      "turn_id": "123e4567-e89b-12d3-a456-426614174000",
      "turn_index": 1,
      "role": "agent",
      "text": "text",
      "timestamp": "text",
      "channel": "voice",
      "content": [
        {
          "type": "text",
          "text": "text",
          "url": "text",
          "media_type": "text",
          "provider_id": "text",
          "metadata": {
            "ANY_ADDITIONAL_PROPERTY": "anything"
          }
        }
      ],
      "tool_calls": [
        {
          "tool_name": "text",
          "call_id": "text",
          "input": {
            "ANY_ADDITIONAL_PROPERTY": "anything"
          },
          "result": "",
          "succeeded": true,
          "duration_ms": 1,
          "error_message": "text"
        }
      ],
      "artifacts": [
        {
          "type": "text",
          "schema_version": "text",
          "payload": "[Circular Reference]"
        }
      ],
      "selected_action_description": "text",
      "state_transitions": [
        {
          "from": "text",
          "to": "text"
        }
      ],
      "available_actions": [
        {
          "description": "text"
        }
      ]
    }
  ],
  "has_more": true,
  "continuation_token": null,
  "truncated": true
}

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}
DELETE /v1/{workspace_id}/conversations/{conversation_id} HTTP/1.1
Host: platform.api-us.concurrence.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*

No content

Closes the conversation permanently. Subsequent turns return 409.


Example: Full REST Flow


WebSocket Sessions

Use Sessions 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

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.

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 and the Sessions API reference.

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.

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). 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 messages enter through managed channel webhooks. The channel validates provider delivery and resolves the destination use case and bound service. SMS uses phone-based routing; WhatsApp uses a business-scoped recipient identifier that is not interchangeable with a phone number. Messages are serialized onto the current non-terminal conversation or create a new one when needed.

WhatsApp's managed free-form send path requires inbound activity within the preceding 23 hours. This delivery window is separate from conversation retention. Template registration does not enable template sending on the current path. See WhatsApp Setup.

SMS suppression applies across the managed business setup and is checked separately from conversation lifecycle. On the supported toll-free path, START/UNSTOP alone does not remove setup-wide suppression. Workspace conversation errors and upstream channel errors have separate contracts; do not assume one universal opt-out status code. See SMS.


World Model Integration

When an entity is bound, a supported runtime can load selected authorized fields from the current world model 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:

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.

Last updated

Was this helpful?