Skip to main content

Chats

Tags: chat, communication, messaging, voice

A Chat is a conversation thread between Members, Agents, and staff. These endpoints create chats, send and read messages, manage participants, place and inspect voice calls, and export conversation history and statistics.

In the product: Chats

Resources

Request and response models used by the endpoints on this page.

AgentMemoryConfig

FieldTypeRequiredDescription
enabledBooleanDefault: False
episodicBooleanDefault: False
preferencesBooleanDefault: True
semanticBooleanDefault: True
summariesBooleanDefault: True

Example:

{
"enabled": false // Boolean, optional, default: False,
"preferences": false // Boolean, optional, default: True,
"semantic": false // Boolean, optional, default: True,
"summaries": false // Boolean, optional, default: True,
"episodic": false // Boolean, optional, default: False
}

BaseAssistantResponse

FieldTypeRequiredDescription
chatModelString
chatModelConfigDict[str, Any]
codeInterpreterEnabledBooleanDefault: False
descriptionString
idInteger
identityString
memoryConfigAgentMemoryConfig
nameString
realtimeModelString
realtimeModelConfigDict[str, Any]
toolsArrayDefault: []
webBrowserEnabledBooleanDefault: False

Example:

{
"id": 0 // Integer, optional,
"name": "string" // String,
"identity": "string" // String, optional,
"description": "string" // String, optional,
"chatModel": "string" // String, optional,
"realtimeModel": "string" // String, optional,
"chatModelConfig": {} // Dict[str, Any], optional,
"realtimeModelConfig": {} // Dict[str, Any], optional,
"tools": "string" // Array<String>, optional, default: [],
"codeInterpreterEnabled": false // Boolean, optional, default: False,
"webBrowserEnabled": false // Boolean, optional, default: False,
"memoryConfig": null // AgentMemoryConfig, optional
}

ChatInterruptResponse

FieldTypeRequiredDescription
checkoutUrlString
descriptionString
guidedLiteral[beat, question]
interruptIdString
openuiMarkupString
paymentCompleteBoolean
priceString
productNameString
specDataObject
specIdInteger
specStatusString
specTitleString
typeString
userPromptString

Example:

{
"type": "string" // String, optional,
"interruptId": "string" // String, optional,
"openuiMarkup": "string" // String, optional,
"description": "string" // String, optional,
"userPrompt": "string" // String, optional,
"guided": null // Literal[beat, question], optional,
"specId": 0 // Integer, optional,
"specTitle": "string" // String, optional,
"specData": {} // Object, optional,
"specStatus": "string" // String, optional,
"checkoutUrl": "string" // String, optional,
"productName": "string" // String, optional,
"price": "string" // String, optional,
"paymentComplete": false // Boolean, optional
}

ChatLabelResponse

FieldTypeRequiredDescription
colorString
descriptionString
idInteger
nameString
slugString
uuidString

Example:

{
"id": 0 // Integer,
"uuid": "string" // String,
"name": "string" // String,
"slug": "string" // String,
"color": "string" // String,
"description": "string" // String
}

ChatParticipantResponse

FieldTypeRequiredDescription
additionalInstructionsString
chatModelString
chatModelConfigDict[str, Any]
memberIdInteger
memberTypeString
nameString
realtimeModelString
realtimeModelConfigDict[str, Any]
roleString

Example:

{
"name": "string" // String,
"additionalInstructions": "string" // String, optional,
"role": "string" // String,
"memberId": 0 // Integer, optional,
"memberType": "string" // String, optional,
"chatModel": "string" // String, optional,
"realtimeModel": "string" // String, optional,
"chatModelConfig": {} // Dict[str, Any], optional,
"realtimeModelConfig": {} // Dict[str, Any], optional
}

ChatResponse

FieldTypeRequiredDescription
activeInteractionModeLiteral[text, push_to_talk, realtime]
archivedBoolean
assignmentIdInteger
canSendAsAssistantBoolean
channelString
chatTypeString
createdAtDateTime
creationSourceLiteral[web_arrival, inbound_channel, outbound_action, operator_create, background_run, qualification_run]
currentTaskNameString
directionLiteral[inbound, outbound, unknown]Default: unknown
folderIdInteger
fromMessageIdInteger
hasRealtimeInteractionBooleanDefault: False
idInteger
inboxIdInteger
initialMessageErrorString
initiatorLiteral[member, workspace, system, unknown]Default: unknown
interruptChatInterruptResponse
isTestBoolean
labelsArray
lastMessageReceivedAtDateTime
messageCountInteger
muteExpiresOnInboundBooleanDefault: False
muteReasonString
mutedBoolean
mutedAtDateTime
mutedByMemberIdInteger
participantsArray
pausedBoolean
pausedAtDateTime
pausedReasonString
phoneNumberIdInteger
siteIdInteger
summaryString
summaryUpdatedAtDateTime
titleString
transportLiteral[web, pstn, sms, whatsapp, email, fax, slack, teams, discord, direct_message, a2a, routine, background, cli, frame, unknown]Default: unknown
updatedAtDateTime
uuidString
workflowIdInteger
workflowRevisionIdInteger

Example:

{
"id": 0 // Integer,
"uuid": "string" // String,
"archived": false // Boolean,
"muted": false // Boolean,
"muteReason": "string" // String, optional,
"muteExpiresOnInbound": false // Boolean, optional, default: False,
"mutedByMemberId": 0 // Integer, optional,
"mutedAt": "2024-01-01T00:00:00Z" // DateTime, optional,
"channel": "string" // String,
"transport": null // Literal[web, pstn, sms, whatsapp, email, fax, slack, teams, discord, direct_message, a2a, routine, background, cli, frame, unknown], optional, default: unknown,
"direction": null // Literal[inbound, outbound, unknown], optional, default: unknown,
"initiator": null // Literal[member, workspace, system, unknown], optional, default: unknown,
"creationSource": null // Literal[web_arrival, inbound_channel, outbound_action, operator_create, background_run, qualification_run], optional,
"hasRealtimeInteraction": false // Boolean, optional, default: False,
"activeInteractionMode": null // Literal[text, push_to_talk, realtime], optional,
"chatType": "string" // String,
"paused": false // Boolean,
"pausedAt": "2024-01-01T00:00:00Z" // DateTime,
"pausedReason": "string" // String,
"assignmentId": 0 // Integer,
"workflowId": 0 // Integer,
"workflowRevisionId": 0 // Integer, optional,
"currentTaskName": "string" // String, optional,
"fromMessageId": 0 // Integer,
"phoneNumberId": 0 // Integer,
"inboxId": 0 // Integer,
"siteId": 0 // Integer,
"folderId": 0 // Integer,
"createdAt": "2024-01-01T00:00:00Z" // DateTime,
"updatedAt": "2024-01-01T00:00:00Z" // DateTime,
"title": "string" // String,
"summary": "string" // String,
"summaryUpdatedAt": "2024-01-01T00:00:00Z" // DateTime,
"lastMessageReceivedAt": "2024-01-01T00:00:00Z" // DateTime,
"messageCount": 0 // Integer,
"participants": [] // Array<ChatParticipantResponse>,
"canSendAsAssistant": false // Boolean,
"labels": [] // Array<ChatLabelResponse>,
"isTest": false // Boolean,
"interrupt": 0 // ChatInterruptResponse, optional,
"initialMessageError": "string" // String, optional
}

ChatTokenUsageResponse

FieldTypeRequiredDescription
audioInputTokensIntegerDefault: 0
audioOutputTokensIntegerDefault: 0
cachedTokensIntegerDefault: 0
completionTokensIntegerDefault: 0
creditsNumberDefault: 0.0
creditsPerHourNumber
creditsRemainingNumber
extraUsageEnabledBoolean
promptTokensIntegerDefault: 0
sttUsageArrayDefault: []
totalTokensIntegerDefault: 0
ttsUsageArrayDefault: []
usageByModelArrayDefault: []
voiceDurationSecondsNumber

Example:

{
"promptTokens": 0 // Integer, optional, default: 0,
"completionTokens": 0 // Integer, optional, default: 0,
"totalTokens": 0 // Integer, optional, default: 0,
"cachedTokens": 0 // Integer, optional, default: 0,
"audioInputTokens": 0 // Integer, optional, default: 0,
"audioOutputTokens": 0 // Integer, optional, default: 0,
"credits": 0.0 // Number, optional, default: 0.0,
"creditsRemaining": 0.0 // Number, optional,
"extraUsageEnabled": false // Boolean, optional,
"usageByModel": [] // Array<ModelUsageResponse>, optional, default: [],
"sttUsage": [] // Array<VoiceUsageResponse>, optional, default: [],
"ttsUsage": [] // Array<VoiceUsageResponse>, optional, default: [],
"voiceDurationSeconds": 0.0 // Number, optional,
"creditsPerHour": 0.0 // Number, optional
}

MemberExperimentGroupSummary

FieldTypeRequiredDescription
groupIdInteger
groupNameString
groupSlugString
isControlBoolean

Example:

{
"groupId": 0 // Integer,
"groupName": "string" // String,
"groupSlug": "string" // String,
"isControl": false // Boolean
}

MemberLabelResponse

FieldTypeRequiredDescription
colorString
descriptionString
idInteger
nameString
slugString
uuidString

Example:

{
"id": 0 // Integer,
"uuid": "string" // String,
"name": "string" // String,
"slug": "string" // String,
"color": "string" // String,
"description": "string" // String
}

MessageTokenUsageResponse

FieldTypeRequiredDescription
cachedTokensIntegerDefault: 0
completionTokensInteger
creditsNumberDefault: 0.0
modelString
promptTokensInteger
totalTokensInteger

Example:

{
"model": "string" // String,
"promptTokens": 0 // Integer,
"completionTokens": 0 // Integer,
"totalTokens": 0 // Integer,
"cachedTokens": 0 // Integer, optional, default: 0,
"credits": 0.0 // Number, optional, default: 0.0
}

ModelUsageResponse

FieldTypeRequiredDescription
audioInputTokensIntegerDefault: 0
audioOutputTokensIntegerDefault: 0
cachedMultiplierNumberDefault: 1.0
cachedTokensIntegerDefault: 0
completionTokensIntegerDefault: 0
creditsNumber
inputMultiplierNumberDefault: 1.0
modelString
multiplierNumber
outputMultiplierNumberDefault: 1.0
promptTokensIntegerDefault: 0
totalTokensInteger

Example:

{
"model": "string" // String,
"promptTokens": 0 // Integer, optional, default: 0,
"completionTokens": 0 // Integer, optional, default: 0,
"totalTokens": 0 // Integer,
"cachedTokens": 0 // Integer, optional, default: 0,
"credits": 0.0 // Number,
"multiplier": 0.0 // Number,
"inputMultiplier": 0.0 // Number, optional, default: 1.0,
"outputMultiplier": 0.0 // Number, optional, default: 1.0,
"cachedMultiplier": 0.0 // Number, optional, default: 1.0,
"audioInputTokens": 0 // Integer, optional, default: 0,
"audioOutputTokens": 0 // Integer, optional, default: 0
}

OpenAIFunctionToolCall

FieldTypeRequiredDescription
functionDict[str, Any]Default: {}
idStringDefault: ``
typeStringDefault: function

Example:

{
"id": "string" // String, optional, default: ,
"type": "string" // String, optional, default: function,
"function": {} // Dict[str, Any], optional, default: {}
}

OpenAIMessageResponse

FieldTypeRequiredDescription
audienceString
audioFileUuidString
audioMetadataDict[str, Any]
childMessageCountIntegerDefault: 0
contentUnion[str, List[Dict[str, Any]], null]
created_atDateTime
email_idInteger
idString
inbox_idInteger
interactionModeLiteral[text, push_to_talk, realtime]
interactionSessionIdString
interactionTurnIdString
isCannedBooleanDefault: False
memberIdInteger
originString
originalContentUnion[str, List[Dict[str, Any]], null]
parentMessageIdString
roleString
smsDeliveryErrorCodeString
smsDeliveryFailureReasonString
smsDeliveryStatusString
smsDeliveryUpdatedAtDateTime
sttMetadataSttMetadataResponse
suggestionsArray
taskIdInteger
tokenUsageMessageTokenUsageResponse
tool_call_idString
tool_callsArrayDefault: []
truncatedAtMsInteger
truncationReasonString
userRatingNumber
userRatingCommentString
uuidString

Example:

{
"id": "string" // String, optional,
"uuid": "string" // String, optional,
"role": "string" // String, optional,
"content": {} // Union[str, List[Dict[str, Any]], null], optional,
"originalContent": {} // Union[str, List[Dict[str, Any]], null], optional,
"truncationReason": "string" // String, optional,
"truncatedAtMs": 0 // Integer, optional,
"sttMetadata": null // SttMetadataResponse, optional,
"tool_calls": [] // Array<OpenAIFunctionToolCall>, optional, default: [],
"tool_call_id": "string" // String, optional,
"suggestions": "string" // Array<String>, optional,
"created_at": "2024-01-01T00:00:00Z" // DateTime, optional,
"email_id": 0 // Integer, optional,
"inbox_id": 0 // Integer, optional,
"tokenUsage": null // MessageTokenUsageResponse, optional,
"memberId": 0 // Integer, optional,
"origin": "string" // String, optional,
"audience": "string" // String, optional,
"isCanned": false // Boolean, optional, default: False,
"childMessageCount": 0 // Integer, optional, default: 0,
"parentMessageId": "string" // String, optional,
"taskId": 0 // Integer, optional,
"interactionSessionId": "string" // String, optional,
"interactionMode": null // Literal[text, push_to_talk, realtime], optional,
"interactionTurnId": "string" // String, optional,
"audioFileUuid": "string" // String, optional,
"audioMetadata": {} // Dict[str, Any], optional,
"smsDeliveryStatus": "string" // String, optional,
"smsDeliveryErrorCode": "string" // String, optional,
"smsDeliveryUpdatedAt": "2024-01-01T00:00:00Z" // DateTime, optional,
"smsDeliveryFailureReason": "string" // String, optional,
"userRating": 0.0 // Number, optional,
"userRatingComment": "string" // String, optional
}

OutboundCallRequest

FieldTypeRequiredDescription
chatIdInteger
phoneNumberIdInteger

Example:

{
"phoneNumberId": 0 // Integer,
"chatId": 0 // Integer
}

PhoneCallResponse

FieldTypeRequiredDescription
agentBaseAssistantResponse
carrierStatusStringTwilio CallStatus / DialCallStatus terminal value (GRA-6416): completed, busy, no-answer, failed, or canceled. Independent of product intent — a successful transfer is terminationReason=forward + carrierStatus=completed; a transfer whose destination was busy is forward + busy.
chatIdInteger
createdAtDateTime
creditsNumber
failureDetailStringTransport / infra failure detail (GRA-6416): connection_reset (Twilio ErrorCode on completed call), stream_reset (media-stream WebSocket reset, e.g. Twilio 31921), never_connected (bridge-leg provisional), timeout, bridge_call_create_failed, bridge_call_create_missing_sid, fax_t30_failed, other closed fax_* receive codes, or error.
finishedAtDateTime
idInteger
inboundBoolean
memberWorkspaceMemberResponse
phoneNumberIdInteger
startedAtDateTime
statusString
terminationReasonStringProduct / app outcome for how the call ended (GRA-6416). Preferred values: hangup (assistant ended), forward (call forwarded), handoff, completed (caller hung up / normal end), policy rejects (voice_disabled, anonymous_rejected, …), blocked_outbound_safety, strict_voice_blocked, voicemail, amd_unknown. Legacy rows may also hold carrier strings (busy, no-answer, …) or transport failures (connection_reset, stream_reset, never_connected); prefer carrierStatus / failureDetail when present. Null for in-progress/unknown calls.
twilioCallIdString
workflowIdIntegerID of the workflow associated with this call, resolved via the chat's assignment (PhoneCall.chat_id -> Chat.assignment_id -> Assignment.workflow_revision_id -> WorkflowRevision.workflow_id). Null for direct human-to-human calls with no chat, or chats with no assignment.

Example:

{
"id": 0 // Integer,
"phoneNumberId": 0 // Integer,
"chatId": 0 // Integer, optional,
"twilioCallId": "string" // String,
"status": "string" // String,
"createdAt": "2024-01-01T00:00:00Z" // DateTime,
"startedAt": "2024-01-01T00:00:00Z" // DateTime, optional,
"finishedAt": "2024-01-01T00:00:00Z" // DateTime, optional,
"member": null // WorkspaceMemberResponse, optional,
"agent": null // BaseAssistantResponse, optional,
"inbound": false // Boolean,
"credits": 0.0 // Number, optional,
"terminationReason": "string" // String, optional, Product / app outcome for how the call ended (GRA-6416). Preferred values: `hangup` (assistant ended), `forward` (call forwarded), `handoff`, `completed` (caller hung up / normal end), policy rejects (`voice_disabled`, `anonymous_rejected`, …), `blocked_outbound_safety`, `strict_voice_blocked`, `voicemail`, `amd_unknown`. Legacy rows may also hold carrier strings (`busy`, `no-answer`, …) or transport failures (`connection_reset`, `stream_reset`, `never_connected`); prefer `carrierStatus` / `failureDetail` when present. Null for in-progress/unknown calls.,
"carrierStatus": "string" // String, optional, Twilio CallStatus / DialCallStatus terminal value (GRA-6416): `completed`, `busy`, `no-answer`, `failed`, or `canceled`. Independent of product intent — a successful transfer is `terminationReason=forward` + `carrierStatus=completed`; a transfer whose destination was busy is `forward` + `busy`.,
"failureDetail": "string" // String, optional, Transport / infra failure detail (GRA-6416): `connection_reset` (Twilio ErrorCode on completed call), `stream_reset` (media-stream WebSocket reset, e.g. Twilio 31921), `never_connected` (bridge-leg provisional), `timeout`, `bridge_call_create_failed`, `bridge_call_create_missing_sid`, `fax_t30_failed`, other closed `fax_*` receive codes, or `error`.,
"workflowId": 0 // Integer, optional, ID of the workflow associated with this call, resolved via the chat's assignment (PhoneCall.chat_id -> Chat.assignment_id -> Assignment.workflow_revision_id -> WorkflowRevision.workflow_id). Null for direct human-to-human calls with no chat, or chats with no assignment.
}

SttMetadataResponse

FieldTypeRequiredDescription
eotConfidenceNumber
meanNumber
minNumber
modelString
wordsArrayDefault: []

Example:

{
"model": "string" // String, optional,
"eotConfidence": 0.0 // Number, optional,
"min": 0.0 // Number, optional,
"mean": 0.0 // Number, optional,
"words": [] // Array<SttWordConfidenceResponse>, optional, default: []
}

SttWordConfidenceResponse

FieldTypeRequiredDescription
cNumber
eNumber
sNumber
wString

Example:

{
"w": "string" // String,
"c": 0.0 // Number,
"s": 0.0 // Number, optional,
"e": 0.0 // Number, optional
}

VoiceUsageResponse

FieldTypeRequiredDescription
characterCountInteger
creditsNumber
durationSecondsNumber
modelString
multiplierNumber

Example:

{
"model": "string" // String,
"credits": 0.0 // Number,
"multiplier": 0.0 // Number,
"durationSeconds": 0.0 // Number, optional,
"characterCount": 0 // Integer, optional
}

WorkspaceMemberResponse

FieldTypeRequiredDescription
accountEnabledBoolean
accountTosAcceptedBoolean
accountUuidString
canLoginBooleanDefault: True
createdAtDateTime
dateOfBirthDate
descriptionString
emailString
emailTrustedBooleanDefault: False
emailVerifiedBooleanDefault: False
experimentGroupMemberExperimentGroupSummary
externalIdString
homeFolderIdInteger
idInteger
isArchivedBooleanDefault: False
isImportProtectedBooleanDefault: False
isOrgOwnerBoolean
isSuperuserBoolean
labelsArrayDefault: []
localeString
memberRoleIdInteger
memberRoleNameString
memberRoleSystemKeyString
memberTypeStringDefault: member
nameString
notifyEmailBoolean
notifySmsBoolean
notifyVoiceBoolean
notifyWhatsappBooleanDefault: False
orgRoleString
ownerIdInteger
ownerNameString
phoneString
phoneTrustedBooleanDefault: False
phoneTypeString
phoneVerifiedBooleanDefault: False
roleString
timezoneString
titleString
tosAcceptedBoolean
updatedAtDateTime
uuidString

Example:

{
"id": 0 // Integer,
"uuid": "string" // String,
"role": "string" // String,
"tosAccepted": false // Boolean,
"accountTosAccepted": false // Boolean,
"accountUuid": "string" // String,
"createdAt": "2024-01-01T00:00:00Z" // DateTime,
"updatedAt": "2024-01-01T00:00:00Z" // DateTime,
"name": "string" // String,
"title": "string" // String, optional,
"description": "string" // String,
"phone": "string" // String,
"phoneType": "string" // String,
"email": "string" // String,
"externalId": "string" // String,
"dateOfBirth": "2024-01-01T00:00:00Z" // Date, optional,
"emailVerified": false // Boolean, optional, default: False,
"phoneVerified": false // Boolean, optional, default: False,
"emailTrusted": false // Boolean, optional, default: False,
"phoneTrusted": false // Boolean, optional, default: False,
"notifySms": false // Boolean,
"notifyEmail": false // Boolean,
"notifyVoice": false // Boolean,
"notifyWhatsapp": false // Boolean, optional, default: False,
"locale": "string" // String,
"memberRoleId": 0 // Integer,
"memberRoleName": "string" // String,
"memberRoleSystemKey": "string" // String, optional,
"homeFolderId": 0 // Integer, optional,
"labels": [] // Array<MemberLabelResponse>, optional, default: [],
"accountEnabled": false // Boolean, optional,
"isOrgOwner": false // Boolean, optional,
"isSuperuser": false // Boolean, optional,
"orgRole": "string" // String, optional,
"timezone": "string" // String, optional,
"memberType": "string" // String, optional, default: member,
"isArchived": false // Boolean, optional, default: False,
"isImportProtected": false // Boolean, optional, default: False,
"ownerId": 0 // Integer, optional,
"ownerName": "string" // String, optional,
"canLogin": false // Boolean, optional, default: True,
"experimentGroup": null // MemberExperimentGroupSummary, optional
}

Endpoints

Create chats

Direct messages

Messages

Chat actions

Voice calls

Statistics & export

Token usage

Guided UI events

Create chats

Create chats, including direct-message chats between two participants.

Create

POST /api/v2/w/{workspace_uuid}/chats

Description:

Create a new chat conversation in the workspace.

Self-access: Any authenticated member can create a chat (becomes the owner).

Creates either a simple chat or, when workflowId is provided, a workflow assignment chat. The assignment is created automatically; callers do not create or pass an assignment first. For phone-voice assignment chats with the assistant enabled, chat creation also places the outbound call.

Request Body:

  • channel: Communication channel (web-chat, phone-sms, phone-voice, email)
  • workflowId: Workflow to run; creates the assignment automatically when provided
  • workflowRevisionId: Published workflow revision to pin this assignment to (defaults to the active revision)
  • memberId: Member this chat is for; defaults to the authenticated member. Naming a human member without chats:write is a 403. A companion device (gr_dev_ credential) naming an Agent on a dm chat is self-access (#25900): the chat is created as the device's bound member's own conversation, with the Agent as the AI participant — no scope needed
  • phoneNumberId: Workspace phone number for SMS/voice; workflow default is used when available
  • inboxId: Workspace inbox for email chats; mutually exclusive with phoneNumberId
  • participants: Additional member participants (supervisor / contributor)
  • assistantKind: Who answers — ai (default) or human. With human the chat's AI (workflow persona, named Agent, or builtin manager assistant) is created suspended (paused=true) rather than omitted, so unpausing hands the chat back to it; a chat with no AI at all stays paused=false
  • assistantMemberId: The assistant member. An Agent member fills the AI slot; a human member is the person answering (implies assistantKind=human)
  • assistantEnabled: Deprecated alias — false means assistantKind=human. It cannot distinguish "no AI exists" from "the AI is paused"
  • assistantChatModelId / assistantRealtimeModelId: Per-chat assistant model overrides
  • initialMessage: Initial operator-typed message for human-answered SMS/email chats
  • title, siteId, chatType: Optional metadata and chat classification
  • isTest: An assertion, not an authority. A chat is a test chat when its counterparty member is one — a TEST member, or an anonymous member minted by a test run — and the server derives the flag from that member rather than from the request body. Sending isTest: true for an ordinary identified member is rejected with 400 and the stable body {"code": "test_provenance_required"}; omitting it (or sending false) does not make a test member's chat live. To create a test chat, create or select a test member first.

Migrating an existing client. Code that declared a test conversation by sending isTest: true on an ordinary member now receives that 400 instead of a chat. Create or select a test member first, then create the chat for that member — the chat comes back with isTest: true derived from the member, and the flag no longer needs to be sent at all.

Response: Returns a ChatResponse object with the newly created chat, including all participants, message counts, and metadata. When an operator-typed initialMessage did not produce a recorded delivery, the chat is still created and returned with initialMessageError set to a PHI-free reason code — a partial success, not an error.

Handling initialMessageError — never blanket-retry. The code says what happened, and two values mean the message may already have reached the recipient. Retrying on either sends a second real message to a patient — a retry here creates a new Chat and a new Message, so the deterministic workflow id that normally de-duplicates a resend does not apply. Branch on the code:

  • dispatch_outcome:accepted_unrecordedthe provider accepted the message; only the local record of it failed. Do not resend. Tell the operator it was delivered but not saved, and reconcile from the provider side.
  • dispatch_outcome:unconfirmed — the durable send was handed off and the hand-off never answered (a timeout, a dropped connection, a lost response). It may or may not have gone out and this server cannot tell. Reconcile before resending: open the chat and check for a delivered message first. Do not present this as a plain failure with a retry button.
  • dispatch_outcome:simulated — a test chat. Nothing was sent, and nothing should be. Not a failure to surface.
  • dispatch_outcome:blocked_policy, dispatch_outcome:suppressed — nothing was sent and a retry resolves the same way. Escalate rather than retry.
  • recipient_sms_opted_out, recipient_no_phone, not_sms_capable, phone_number_archived, dispatch_outcome:blocked_validation, dispatch_outcome:disabled — the recipient or sender cannot receive on this channel. Nothing was sent; an unchanged retry skips again. Fix the routing data first.
  • anything else (send_failed, dispatch_outcome:failed, chat_missing_phone_number_id, chat_missing_inbox_id, inbox_not_found, post_commit_chat_or_member_missing, channel_not_dispatchable, email_service_returned_none) — nothing reached the recipient, so reloading the chat and resending is safe. send_failed now means that narrowly: a send the provider or the durable hand-off refused, or a failure before either was engaged. A hand-off that never answered is dispatch_outcome:unconfirmed above, not this.

Permissions: Requires authenticated member session. Chat ownership is automatically assigned to the creator.

Error Responses:

  • 400 Bad Request: Invalid request parameters or missing required fields
  • 401 Unauthorized: Missing or invalid authentication
  • 404 Not Found: Referenced workflow, member, or assistant member not found
  • 500 Internal Server Error: Database or processing error

Related Endpoints:

  • GET /chats - List all chats
  • GET /chats/{chat_id} - Get chat details
  • PUT /chats/{chat_id} - Update chat properties

Authentication: Requires workspace member

Parameters:

  • chat (CreateChatRequest)

Response: See ChatResponse


Direct messages

Find or create the direct-message chat between two members.

Dm

POST /api/v2/w/{workspace_uuid}/chats/dm

Description:

Find or create a DM chat with a specific member.

Returns the existing DM chat between the current member and the target member, or creates a new one if none exists.

Self-access: a member may always start their own DM with an agent — it is their own conversation, so no scope is required (matching record self-access, where creating your own record needs no records:write). Initiating a DM with another human member is cross-member contact and requires dm:write.

Authentication: Requires workspace member

Parameters:

Response: See ChatResponse


Messages

Read and send messages in a chat, rate responses, and inspect tool-call messages.

Messages

GET /api/v2/w/{workspace_uuid}/chats/{chat_id}/messages

Description:

Get messages from a specific chat with pagination.

Retrieves the message history for a chat in OpenAI-compatible format. Messages include content, role (user/assistant/system), tool calls, and metadata. Results are paginated for efficient loading of long conversations.

Path Parameters:

  • id: Chat ID to retrieve messages from

Query Parameters:

  • limit: Maximum number of messages to return (1-100, default: 10)
  • offset: Number of messages to skip for pagination (default: 0)
  • before: Inclusive timestamp bound for a historical window (optional)

Response: Returns an array of OpenAIMessageResponse objects in chronological order (oldest first), each containing message content, role, timestamps, and tool call data.

Permissions: Self-access: Users can view messages in their own chats. Admin access: CHATS_READ scope required to view other members' chat messages.

Error Responses:

  • 401 Unauthorized: Missing or invalid authentication
  • 403 Forbidden: User does not have permission to view this chat
  • 404 Not Found: Chat not found

Related Endpoints:

  • GET /chats/{chat_id} - Get chat details
  • GET /chats/{chat_id}/tool-calls/{call_id}/messages - Get tool call messages
  • POST /chats/{chat_id}/send-assistant-message - Send a message as assistant

Authentication: Requires workspace member

Parameters:

  • chat_id (int | str)
  • limit (Integer) — default: 10, min: 1, max: 100
  • offset (Integer) — default: 0, min: 0
  • before (Annotated[datetime , Query(description='Inclusive ISO-8601 upper bound for a historical message window')])

Response: List of OpenAIMessageResponse


Messages

POST /api/v2/w/{workspace_uuid}/chats/{chat_id}/messages

Description:

Send a user message in a chat.

For DM chats with human recipients, triggers SendDmNotificationWorkflow. Agent DM responses are handled via the AGUI streaming endpoint.

Authorization is participation, checked in SQL below: the caller must hold a role="user" participant row on this chat. There is no cross-member capability here to gate — the message minted below is always authored by member_session.member_id, so this route cannot post on anybody else's behalf no matter what scopes the caller holds.

It therefore takes no coarse scope. The former CHATS_WRITE dependency gated pure self-access, which inverts the model this module's docstring states and which SITE_SESSION_SCOPES spells out in lib/context/scopes.py: chats:* exists for CROSS-MEMBER administrative access, and a member reaches their own chat with no scope at all. The coarse gate made every deliberately scope-clamped credential — companion devices (DEVICE_TOKEN_SCOPES) and site sessions (SITE_SESSION_SCOPES), neither of which can ever hold chats:write — structurally unable to speak in a chat they own. See docs/decisions/api/2026-09-01-2-own-chat-participation-is-not-scope-gated.md

Authentication: Requires workspace member

Parameters:

Response: See OpenAIMessageResponse


Children

GET /api/v2/w/{workspace_uuid}/chats/{chat_id}/messages/{message_uuid}/children

Description:

Get the background-work thread hanging off a message.

When a tool trajectory outgrows its turn it detaches and runs in the background (ADR-11 / forked trajectories), writing its messages as a CHILD THREAD parented at the fork point rather than into the main transcript. Main-history reads filter those out (parent_message_id IS NULL), so this endpoint is how the "show work" affordance fetches them — lazily, only when a member expands a message whose childMessageCount is non-zero.

Path Parameters:

  • chat_id: Chat containing the fork point
  • message_uuid: The fork point's message id (OpenAIMessageResponse.id)

Query Parameters:

  • limit: Maximum messages to return (1-1000, default: 200)
  • offset: Messages to skip for pagination (default: 0)

Response: The child thread in chronological order. Empty when the message has no background thread (or the uuid is not in this chat).

Permissions: Self-access: members can view threads in their own chats. Admin access: CHATS_READ scope for other members' chats.

Related Endpoints:

  • GET /chats/{chat_id}/messages — main transcript (excludes children)
  • GET /chats/{chat_id}/tool-calls/{call_id}/messages — ReACT loop of one tool call

Authentication: Requires workspace member

Parameters:

  • chat_id (int | str)
  • message_uuid (String)
  • limit (Integer) — default: 200, min: 1, max: 1000
  • offset (Integer) — default: 0, min: 0

Response: List of OpenAIMessageResponse


Rate

POST /api/v2/w/{workspace_uuid}/chats/{chat_id}/messages/{message_uuid}/rate

Description:

Rate an assistant message for quality feedback.

Persists the rating to the workspace-plane chat_feedback table (durable, customer-visible). Langfuse scoring is best-effort observability on top and never affects the API response once the workspace row is written — missing traces, unconfigured Langfuse, or Langfuse errors are logged, not returned to the client.

Path Parameters:

  • chat_id: Chat ID containing the message
  • message_uuid: UUID of the message to rate

Request Body:

  • rating: Numeric rating value (e.g., 1.0 for positive, 0.0 for negative)
  • comment: Optional text comment explaining the rating

Response: Returns {"status": "success"} when the durable workspace rating was recorded. Langfuse outcome is never exposed to the client.

Permissions: Self-access: Users can rate messages in their own chats. Admin access: CHATS_WRITE scope required to rate other members' chat messages.

Error Responses:

  • 400 Bad Request: Message is not an assistant message or integrity check failed
  • 401 Unauthorized: Missing or invalid authentication
  • 403 Forbidden: User does not have permission to rate messages in this chat
  • 404 Not Found: Chat or message not found

Related Endpoints:

  • GET /chats/{chat_id}/messages - Get chat messages to rate
  • POST /chats/{chat_id}/send-assistant-message - Send assistant messages

Authentication: Requires workspace member

Parameters:

  • chat_id (int | str)
  • message_uuid (String)
  • rating_data (RateMessageRequest)

Messages

GET /api/v2/w/{workspace_uuid}/chats/{chat_id}/tool-calls/{call_id}/messages

Description:

Get messages from a specific tool call execution.

Retrieves the detailed message history from a tool call's ReACT loop execution. This includes sub-agent messages, tool invocations, and results from custom tools that run their own conversation loops.

Path Parameters:

  • id: Chat ID containing the tool call
  • call_id: Tool call ID to retrieve messages from

Query Parameters:

  • limit: Maximum number of messages to return (1-1000, default: 100)
  • offset: Number of messages to skip for pagination (default: 0)

Response: Returns an array of OpenAIMessageResponse objects representing the internal conversation flow within the tool execution, useful for debugging and transparency.

Permissions: Self-access: Users can view tool call messages in their own chats. Admin access: CHATS_READ scope required to view other members' chat messages.

Error Responses:

  • 401 Unauthorized: Missing or invalid authentication
  • 403 Forbidden: User does not have permission to view this chat
  • 404 Not Found: Chat or tool call not found

Related Endpoints:

  • GET /chats/{chat_id}/messages - Get main chat messages
  • GET /chats/{chat_id}/tools - Get available tools for chat

Authentication: Requires workspace member

Parameters:

  • chat_id (int | str)
  • call_id (String)
  • limit (Integer) — default: 100, min: 1, max: 1000
  • offset (Integer) — default: 0, min: 0

Response: List of OpenAIMessageResponse


Chat actions

Act on an existing chat: manage participants, trigger an Agent reply, or summarize.

Participants

POST /api/v2/w/{workspace_uuid}/chats/{chat_id}/participants

Description:

Add a participant to a chat with a specified role.

Allows workspace managers to add members to chats with various roles including contributors. Contributors can be consulted via contributor_input_request tool during conversations.

Path Parameters:

  • chat_id: Chat ID to add participant to

Request Body:

  • memberId: ID of the member to add as a participant
  • role: user | assistant | contributor | supervisor (default: "contributor"). operator is not a participant role — a human take-over is an assistant participant carrying that member's id.

Response: Returns a success status, message, and the ID of the created participant.

Permissions: Requires chats:admin scope. The route declared chats:write while ChatManagementService.add_participant enforced chats:admin, so a chats:write-only caller passed the route gate and then got a 403 from the service with documentation claiming otherwise. chats:admin is the real requirement (adding a participant grants that member access to the chat's PHI); the service-layer check is retained as defense in depth.

Error Responses:

  • 401 Unauthorized: Missing or invalid authentication
  • 403 Forbidden: User lacks chats:admin scope
  • 404 Not Found: Chat or member not found
  • 400 Bad Request: Invalid role or participant already exists
  • 422 Unprocessable Entity: role outside the participant-role vocabulary

Related Endpoints:

  • GET /chats/{id} - Get chat details including participants
  • GET /members - List workspace members

Authorization: Requires chats:admin scope

Parameters:

Response: See ChatParticipantAddedResponse


Send Assistant Message

POST /api/v2/w/{workspace_uuid}/chats/{chat_id}/send-assistant-message

Description:

Send a message as the assistant in a chat conversation.

Allows workspace managers to manually send messages on behalf of the chat's assistant. Supports SMS and email channels with automatic delivery. Can optionally include tool calls which will be executed before message delivery. Records the attention clear (the person answered) when used.

Path Parameters:

  • chat_id: Chat ID to send the message in

Request Body:

  • message: Message content to send (supports markdown for email)
  • tool_calls: Optional array of tool calls to execute with the message

Response: Returns a success confirmation with the created message details including ID, UUID, role, content, and timestamp.

success and message describe the stored message, which is persisted on every non-error path. The provider leg is reported separately in delivery_outcome, a closed vocabulary (dispatched, simulated, blocked_validation, blocked_policy, disabled, suppressed, accepted_unrecorded), or null when no send was owed. simulated means the chat is a test chat and nothing was sent, which is correct rather than a failure. accepted_unrecorded means the provider accepted the message and only the local record failed — do not resend. A provider refusal is a 500, not an outcome value.

Permissions: Requires chats:admin scope to send messages as the assistant.

Error Responses:

  • 400 Bad Request: Invalid chat configuration, missing phone/email, or notifications disabled
  • 401 Unauthorized: Missing or invalid authentication
  • 403 Forbidden: User lacks chats:admin scope
  • 404 Not Found: Chat, assistant, or participant not found
  • 500 Internal Server Error: Failed to send SMS/email or execute tools

Related Endpoints:

  • GET /chats/{id}/messages - Get chat message history
  • POST /chats/{id}/messages/{uuid}/rate - Rate an assistant message
  • GET /chats/{id}/tools - Get available tools for the chat

Authorization: Requires chats:admin scope

Parameters:


Summarize

POST /api/v2/w/{workspace_uuid}/chats/{chat_id}/summarize

Description:

Generate an AI summary of the chat conversation.

Creates an AI-generated summary of the chat's message history, updating the chat's title and summary fields. Optionally analyzes conversation content to automatically update chat labels. Summary generation happens synchronously within the request.

Path Parameters:

  • chat_id: Chat ID to summarize

Query Parameters:

  • update_labels: If true, also updates chat labels based on conversation content (default: false)

Response: Returns the updated ChatResponse object with new title, summary, summary_updated_at timestamp, and optionally updated labels.

Permissions: Self-access: Users can summarize their own chats. Admin access: CHATS_WRITE scope required to summarize other members' chats.

Error Responses:

  • 401 Unauthorized: Missing or invalid authentication
  • 403 Forbidden: User does not have permission to summarize this chat
  • 404 Not Found: Chat not found
  • 500 Internal Server Error: AI summarization failed

Related Endpoints:

  • GET /chats/{id} - Get chat details including current summary
  • GET /chats/{id}/messages - View messages being summarized
  • PUT /chats/{id} - Update chat manually

Authentication: Requires workspace member

Parameters:

  • chat_id (Integer)
  • update_labels (Boolean) — default: False

Response: See ChatResponse


Voice calls

Place outbound voice calls on a chat and inspect its phone calls.

Outbound Call Handler

GET /api/v2/w/{workspace_uuid}/chats/outbound-call-handler/{phone_number_id}/{chat_id}/{phone_call_id}

Description:

Handle Twilio webhook for outbound calls.

The phone_call_id path parameter references a PhoneCall row that was pre-created by initiate_outbound_call_for_chat() in PENDING state. This handler finds that row and updates agent_id, started_at, and status now that the call has actually connected to Twilio's stream.

Auth: this is a Twilio-fetched webhook, authenticated by the request signature (validate_twilio_request inside _handle_outbound_call_impl), NOT by an org/member session. It therefore depends on require_ws_db_public, which omits the org-SSO gate. Using the gated require_ws_db here 401'd Twilio's unauthenticated fetch on ssoOnly orgs (Bearer realm="org" challenge), which Twilio surfaced as error 11200 and spoke to the callee as "an application error has occurred" — killing every outbound call for those orgs before the handler ran. Keep this on the public dependency.

Authentication: Requires a valid Twilio request signature

Parameters:

  • chat_id (Integer)
  • phone_number_id (Integer)
  • phone_call_id (Integer)

Outbound Call Handler

POST /api/v2/w/{workspace_uuid}/chats/outbound-call-handler/{phone_number_id}/{chat_id}/{phone_call_id}

Description:

Handle Twilio webhook for outbound calls.

The phone_call_id path parameter references a PhoneCall row that was pre-created by initiate_outbound_call_for_chat() in PENDING state. This handler finds that row and updates agent_id, started_at, and status now that the call has actually connected to Twilio's stream.

Auth: this is a Twilio-fetched webhook, authenticated by the request signature (validate_twilio_request inside _handle_outbound_call_impl), NOT by an org/member session. It therefore depends on require_ws_db_public, which omits the org-SSO gate. Using the gated require_ws_db here 401'd Twilio's unauthenticated fetch on ssoOnly orgs (Bearer realm="org" challenge), which Twilio surfaced as error 11200 and spoke to the callee as "an application error has occurred" — killing every outbound call for those orgs before the handler ran. Keep this on the public dependency.

Authentication: Requires a valid Twilio request signature

Parameters:

  • chat_id (Integer)
  • phone_number_id (Integer)
  • phone_call_id (Integer)

Outbound Call

POST /api/v2/w/{workspace_uuid}/chats/{chat_id}/outbound-call

Description:

Initiate an outbound phone call for a chat.

Starts an outbound voice call from the assistant to the chat participant using Twilio. The call will be connected to the workspace's configured phone number and handled by the chat's assigned assistant with real-time audio streaming.

Path Parameters:

  • chat_id: Chat ID to initiate the call for

Request Body:

  • phoneNumberId: ID of the workspace phone number to use for the outbound call

Response: Returns a status object confirming the call was initiated successfully.

Permissions: Requires workspace management permissions and sufficient phone call budget.

Error Responses:

  • 400 Bad Request: Invalid phone number ID or chat configuration
  • 401 Unauthorized: Missing or invalid authentication
  • 403 Forbidden: Insufficient permissions or exceeded budget limits
  • 404 Not Found: Chat or phone number not found
  • 500 Internal Server Error: Failed to initiate call with Twilio

Related Endpoints:

  • GET /chats/{id} - Get chat details
  • POST /chats - Create a new chat for phone conversations

Authorization: Requires phones:write scope

Parameters:

Response: See OutboundDialResult


Phone Calls

GET /api/v2/w/{workspace_uuid}/chats/{chat_id}/phone-calls

Description:

Get phone calls associated with a chat.

Returns all phone calls linked to the given chat, ordered by most recent first. Used by the chat detail view to display call termination information.

Authorization: Requires chats:read scope

Parameters:

  • chat_id (Integer)

Response: List of PhoneCallResponse


Statistics & export

Chat statistics, needs-response queues, and conversation exports.

Export

GET /api/v2/w/{workspace_uuid}/chats/export

Description:

Export chats as CSV with filtering applied.

Exports all chats matching the current filters to a CSV file. Respects the same filtering parameters as the list_chats endpoint but returns all matching results (not paginated). Useful for bulk analysis, reporting, and integration with external tools.

Query Parameters: All the same filtering parameters as GET /chats:

  • sortBy, sortOrder: Sort order for consistent output
  • mine, archived, needsResponse, paused: Status filters
  • channel, chatType, memberId, assistantMemberId: Type and participant filters
  • search: Text search across chat content and metadata
  • isTest: Filter test chats
  • isAnonymous: Owner-anonymity scope, identical to GET /chats. true = anonymous-owner chats only (requires chats:admin); false = known-owner chats only; omitted = no owner-anonymity filter, so a chats:admin caller exports both. The export and the list therefore return the same chat set for the same request.
  • filters: JSON-encoded advanced filter rules
  • createdAfter: ISO-8601 timestamp with timezone offset — only export chats created on or after this instant
  • createdBefore: ISO-8601 timestamp with timezone offset — only export chats created on or before this instant

Response: Returns a CSV file with chat data including metadata, participants, and status.

Permissions: Requires chats:read scope. Members without this scope cannot bulk-export chats. Filtering to anonymous chats (isAnonymous=true) additionally requires chats:admin. Members without chats:admin never receive anonymous-owner rows — that is a server-enforced authorization boundary, not a removable filter.

Authorization: Requires chats:read scope

Parameters:

  • sortBy (ChatSortField)
  • sortOrder (SortOrder)
  • mine (Boolean)
  • archived (Boolean)
  • needsResponse (Boolean)
  • paused (Boolean)
  • channel (String)
  • chatType (String)
  • memberId (Integer)
  • search (String)
  • filters (String)
  • query (String)
  • isTest (Boolean)
  • isAnonymous (Boolean)
  • assistantMemberId (Integer)
  • createdAfter (datetime)
  • createdBefore (datetime)

Needs Response

GET /api/v2/w/{workspace_uuid}/chats/needs-response

Description:

Get IDs of chats that need a response.

Returns a lightweight list of chat IDs that require attention from the workspace team. DERIVED from facts — chat_needs_response_clause() — not from a stored flag, so a chat handed back to the AI or answered through a path that never cleared the old column drops out on its own.

Response: Returns an array of chat IDs (integers) that need responses. Empty array if none found.

Permissions: Self-access: Users see their own chats needing response. Admin access: CHATS_READ scope required to see all chats needing response. Private chats (manager / dm / agent_job) stay owner-or-participant only, for every caller.

Authentication: Requires workspace member


Daily Creation

GET /api/v2/w/{workspace_uuid}/chats/stats/daily-creation

Description:

Get daily chat creation statistics for the last 30 days.

Returns aggregated chat creation counts grouped by date and chat type for the past 30 days. This data is useful for visualizing chat activity trends and patterns by type.

Query Parameters:

  • archived: Filter by archived status (true/false, default: excludes archived)
  • isTest: Filter by test status (true/false, default: excludes test chats)

Response: Returns a dictionary with dailyStats array containing objects with:

  • date: YYYY-MM-DD format
  • count: total chat count
  • assignment, manager, dm, testing: breakdown by type

Permissions:

  • Admin users see all workspace chats
  • Regular users see only their own chats

Authentication: Requires workspace member

Parameters:

  • archived (Boolean)
  • isTest (Boolean)

Tool Call Names

GET /api/v2/w/{workspace_uuid}/chats/tool-call-names

Description:

Get distinct tool call names used across workspace chats.

Returns an alphabetically sorted list of unique tool call names from all messages in the workspace. Useful for populating filter dropdowns.

Permissions:

  • Admin users see tool calls from all workspace chats
  • Regular users see tool calls only from their own chats

Authentication: Requires workspace member


Token usage

Token usage recorded for a chat.

Token Usage

GET /api/v2/w/{workspace_uuid}/chats/{chat_id}/token-usage

Description:

Get aggregated token usage statistics for a specific chat.

Returns the total prompt tokens, completion tokens, total tokens, cached tokens, and credits used by all messages in this chat, including STT and TTS usage from voice calls.

Path Parameters:

  • chat_id: Chat ID to retrieve token usage for

Response: Returns a ChatTokenUsageResponse with aggregated token counts and credits.

Permissions: Self-access: Users can view token usage for their own chats. Admin access: CHATS_READ scope required to view other members' chat usage.

Error Responses:

  • 401 Unauthorized: Missing or invalid authentication
  • 403 Forbidden: User does not have permission to view this chat
  • 404 Not Found: Chat not found

Authentication: Requires workspace member

Parameters:

  • chat_id (Integer)

Response: See ChatTokenUsageResponse


Guided UI events

Stage directions a guided script publishes to the app the member is looking at: take them to a page, point at a control, clear the highlight. Published and returned — what the guide says blocks the script instead, and travels as a parked prompt rather than as an event here.

Ui Events

POST /api/v2/w/{workspace_uuid}/chats/{chat_id}/ui-events

Description:

Publish a batch of stage directions on the chat's realtime channel.

202 rather than 200: this hands the events to subscribers and returns. It does not — and cannot — report that anyone was listening. A script whose tour surface has been closed should carry on and finish its work, not stall waiting for an audience.

Request body: {"events": [...]} — 1 to 20 events, applied in the order given. Each event is one of:

  • {"kind": "navigate", "appUri": "app:page:workflows"} — a page from the closed registry the navigate_to_admin_page tool also validates against. An unknown URI rejects the whole batch rather than dropping the one event: a script that narrated "here is the workflow list" and then failed to navigate leaves the member reading a caption for a page they are not on.
  • {"kind": "highlight", "anchor": "workflows.create-button", "note": "…"}anchor is a declared data-tour-id, never a CSS selector, and must match ^[a-z0-9]+(?:[.-][a-z0-9]+)*$ (≤128 chars); note is an optional caption of ≤200 characters.
  • {"kind": "clear_highlight"} — idempotent.

There is deliberately no say event. Narration is a beat: it blocks the script until the member advances it, so it travels as a parked prompt (askUser) rather than as an event here.

Response: {"published": <count>} — how many events were handed to the channel, never how many were rendered.

Permissions: CHATS_WRITE (checked by the request model), plus the ordinary chat-access resolution on the chat itself.

Authentication: Requires workspace member

Parameters:

  • chat_id (Integer)
  • request_body (PublishUiEventsRequest)