Conversations API

Conversations is the shared dialogue layer for the platform: one place for human↔human, human↔agent, and agent↔agent threads, with conversation participants (users, agents, operators, contacts), IAM-scoped project and conversation zones, durable activity events for audit and automations, and WebSocket streams for conversation events and per-principal list refreshes. Conversation projects live at `org/{company_id}/conversation-project/{project_id}` and conversation threads live at `org/{company_id}/conversation/{conversation_id}`; project_id is a product grouping field, not a nested IAM path. Conversations can be linked to an external subject (subject_type/subject_id, e.g. a workflow item) for discovery, opened toward outbound contacts (to), and carry per-caller capability flags so UIs can hide actions the principal cannot perform. Use Channel mode to create outbound email conversations with channel='email', from_channel_address_id, to[], and first_message. Non-workflow create and reply require channels:addresses:send on the selected Channels address. Messages can carry rich tool/model metadata, structured content blocks, and Buckets-backed attachments. Inbound email persists placeholder AttachmentRefs immediately, then streams message.updated while each import advances through pending, downloading, uploading, and a terminal or processing state. Listing the company feed is grant-filtered: results are limited to conversations the caller holds conversations:conversations:view on (participation grants view, but an explicit grant does too), while by-subject discovery is intentionally not participant-gated. Each project and conversation also exposes a paginated activity feed and a single-event lookup. Activity and automations catalogs expose event slugs such as conversations.conversation.created, conversations.conversation.updated, conversations.conversation.deleted, conversations.message.added, conversations.participant.added, conversations.participant.removed, conversations.project.created, conversations.project.updated, conversations.project.deleted, plus conversations.attachment.uploaded and conversations.attachment.failed. Conversations owns episodic continuity: bounded granular notes, one living digest, an embedding-only notes/ export, close/idle settlement, and participant/privacy-scoped prior-conversation digest recall used by Agents and inferred starters. The public notes tool returns granular notes; digest/context/recall adapters are internal service contracts. IAM checks use the conversations:* permission namespace (conversations:projects:*, conversations:conversations:*, conversations:messages:create, conversations:participants:manage, conversations:attachments:create, conversations:activity:view, conversations:settings:{view,manage}, conversations:privacy:manage, conversations:notes:{view,manage}, conversations:permissions:manage) on org, conversation-project, and conversation resources (see each route). Holders of agents:conversations:view on a participating agent get derived read access to that agent's public conversations.

Base URL/api/v1/conversations

Automation triggers: view every Conversations event, payload field, and predicate.

Projects

Projects group conversations under a company (slug, branding, optional Buckets folder, agent system-prompt append). Use them when you want a stable container for many threads rather than ad-hoc DMs. Projects are created and listed per company; id-addressed reads, updates, and deletes use the `/projects/{project_id}` routes.

POST/api/v1/conversations/companies/{company_id}/projects

Create Project

Create a project for the company. Writes a conversations.project.created activity event.

Bearer token required. Permission: conversations:projects:create on org/{company_id}

Path Parameters

NameTypeDescription
company_id*
string (UUID)Organization ID

Request Body

NameTypeDescription
company_id*
string (UUID)Must match the path company_id
slug*
stringUnique slug within the company
name*
stringDisplay name
description
string | nullOptional description
icon
string | nullOptional icon key or URL
color
string | nullOptional theme color
bucket_id
string (UUID) | nullLinked Buckets folder
system_prompt_append
string | nullText appended to agent system prompts in this project

Response Fields

NameTypeDescription
id*
UUIDProject ID
company_id*
UUIDOrganization ID
slug*
stringSlug
name*
stringDisplay name
description*
string | nullDescription
icon*
string | nullIcon
color*
string | nullColor
bucket_id*
UUID | nullFolder link
system_prompt_append*
string | nullPrompt append
created_by*
UUID | nullCreator principal
conversation_count*
integerNumber of conversations in this project (defaults to 0)
created_at*
datetimeCreated at
updated_at*
datetimeUpdated at
curl -X POST https://platform.ergondata.ai/api/v1/conversations/companies/{company_id}/projects \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{
    "company_id": "c0ffee00-cafe-babe-dead-beefcafebabe",
    "slug": "customer-success",
    "name": "Customer Success",
    "description": "Shared inbox and agent threads"
  }'

Response

201 Created
{
  "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "company_id": "c0ffee00-cafe-babe-dead-beefcafebabe",
  "slug": "customer-success",
  "name": "Customer Success",
  "description": "Shared inbox and agent threads",
  "icon": null,
  "color": null,
  "bucket_id": null,
  "system_prompt_append": null,
  "created_by": "u1111111-2222-3333-4444-555555555555",
  "conversation_count": 0,
  "created_at": "2026-04-21T10:00:00Z",
  "updated_at": "2026-04-21T10:00:00Z"
}
GET/api/v1/conversations/companies/{company_id}/projects

List Projects

Paginated projects for a company, ordered by name, each with a conversation_count.

Bearer token required. Permission: conversations:projects:view on each conversation-project resource

Path Parameters

NameTypeDescription
company_id*
string (UUID)Organization ID

Query Parameters

NameTypeDescription
limit
integerPage size (1–200)Default: 50
offset
integerOffsetDefault: 0
q
string | nullCase-insensitive name filter

Response Fields

NameTypeDescription
items*
ProjectOut[]Project rows
total*
integerTotal matching projects
curl "https://platform.ergondata.ai/api/v1/conversations/companies/{company_id}/projects?limit=50&offset=0" \
  -H "Authorization: Bearer {token}"

Response

200 OK
{
  "items": [
    {
      "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
      "company_id": "c0ffee00-cafe-babe-dead-beefcafebabe",
      "slug": "customer-success",
      "name": "Customer Success",
      "description": null,
      "icon": null,
      "color": null,
      "bucket_id": null,
      "system_prompt_append": null,
      "created_by": "u1111111-2222-3333-4444-555555555555",
      "conversation_count": 3,
      "created_at": "2026-04-21T10:00:00Z",
      "updated_at": "2026-04-21T10:00:00Z"
    }
  ],
  "total": 1
}
GET/api/v1/conversations/projects/{project_id}

Get Project

Fetch one project with its live conversation_count.

Bearer token required. Permission: conversations:projects:view on org/{company_id}/conversation-project/{project_id}

Path Parameters

NameTypeDescription
project_id*
string (UUID)Project ID

Response Fields

NameTypeDescription
(ProjectOut)*
objectSame fields as the create-project response
curl https://platform.ergondata.ai/api/v1/conversations/projects/{project_id} \
  -H "Authorization: Bearer {token}"

Response

200 OK
{
  "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "company_id": "c0ffee00-cafe-babe-dead-beefcafebabe",
  "slug": "customer-success",
  "name": "Customer Success",
  "description": null,
  "icon": null,
  "color": null,
  "bucket_id": null,
  "system_prompt_append": null,
  "created_by": "u1111111-2222-3333-4444-555555555555",
  "conversation_count": 3,
  "created_at": "2026-04-21T10:00:00Z",
  "updated_at": "2026-04-21T10:00:00Z"
}
PATCH/api/v1/conversations/projects/{project_id}

Update Project

Patch project fields. Writes conversations.project.updated.

Bearer token required. Permission: conversations:projects:edit on org/{company_id}/conversation-project/{project_id}

Path Parameters

NameTypeDescription
project_id*
string (UUID)Project ID

Request Body

NameTypeDescription
name
string | nullDisplay name
slug
string | nullSlug
description
string | nullDescription
icon
string | nullIcon
color
string | nullColor
bucket_id
string (UUID) | nullFolder link
system_prompt_append
string | nullPrompt append

Response Fields

NameTypeDescription
(ProjectOut)*
objectUpdated project, same fields as the create-project response
curl -X PATCH https://platform.ergondata.ai/api/v1/conversations/projects/{project_id} \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{"name": "Customer Success (EMEA)"}'

Response

200 OK
{
  "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "company_id": "c0ffee00-cafe-babe-dead-beefcafebabe",
  "slug": "customer-success",
  "name": "Customer Success (EMEA)",
  "description": null,
  "icon": null,
  "color": null,
  "bucket_id": null,
  "system_prompt_append": null,
  "created_by": "u1111111-2222-3333-4444-555555555555",
  "conversation_count": 3,
  "created_at": "2026-04-21T10:00:00Z",
  "updated_at": "2026-04-21T11:30:00Z"
}
DELETE/api/v1/conversations/projects/{project_id}

Delete Project

Remove a project after logging conversations.project.deleted.

Bearer token required. Permission: conversations:projects:delete on org/{company_id}/conversation-project/{project_id}

Path Parameters

NameTypeDescription
project_id*
string (UUID)Project ID
curl -X DELETE https://platform.ergondata.ai/api/v1/conversations/projects/{project_id} \
  -H "Authorization: Bearer {token}"

Response

204 No Content

Conversations

Create and manage threads. DMs require exactly two principals and auto-deduplicate for human-only pairs (no agent on either side) so the same two people do not get parallel empty threads; agent-inclusive DMs always create a new row. Group chats need at least two participants and a title. For Channel mode email, supply channel='email', from_channel_address_id, to[], and normally first_message; non-workflow create and reply require channels:addresses:send through Channels. The company feed is filtered to conversations where the caller is an active participant, while by-subject discovery is not participant-gated.

POST/api/v1/conversations/conversations

Create Conversation

Open a DM or group conversation, optionally seeding a first message, linking a subject, or addressing outbound contacts. Channel mode email uses channel='email', from_channel_address_id, to[], and first_message. Non-workflow email requires channels:addresses:send; item-bound outbound threads instead require workflows:items:start_conversation.

Bearer token required. Permission: conversations:conversations:create on org/{company_id} (or on the target conversation-project for project threads)

Request Body

NameTypeDescription
company_id*
string (UUID)Tenant
type
stringdm or groupDefault: dm
channel
stringDelivery mode: direct or email (Channel mode)Default: direct
project_id
string (UUID) | nullOptional project scope
title
string | nullRequired for group conversations
metadata
object | nullOpaque JSON metadata. One reserved key is read by the agents runtime: agent_model_override = { "<agent_id>": { "llm_config_id": "<uuid>", "model_slug": "<slug>" } } pins, per conversation, which model a given agent answers with (sticky until changed). The override is honored only when that agent holds agents:llm-configs:use on the config and it belongs to the agent's company; otherwise the runtime falls back to the agent's default model.
participants
ParticipantCreate[]Each item: principal_id (UUID), principal_type (user | agent | operator | contact), role (default member). Defaults to []
subject_type
string | nullExternal subject type to link (e.g. workflow.item)
subject_id
string (UUID) | nullExternal subject id to link
to
ContactRecipient[]Outbound contact recipients, each with address (required) and optional display_name. Defaults to []
first_message
FirstMessage | nullOptional initial message: content (string | null) and attachments (AttachmentRef[] | null)
from_channel_address_id
string (UUID) | nullRequired for email. Channels address used to send; non-workflow callers need channels:addresses:send
auto_grant_enabled
booleanAuto-grant view access to added participantsDefault: true
privacy_level
string | nullpublic or private. Omitted defaults to private. Ignored when the request dedupes into an existing human DM (the existing row's setting wins). Always-public invariant: all-agent conversations (only agent participants) and any conversation that includes a privacy-locked agent are forced public regardless of this value. Also patchable later via PATCH /conversations/{id}
note_taking_cadence
object | nullPer-conversation cadence override (keys: every_messages, every_minutes, every_tokens, min_messages). Omitted or null inherits the company default. Also patchable later via PATCH /conversations/{id}
starter_mode
"user-talks-first" | "preset" | "infer"Opening behavior. Presets are deterministic; infer uses authorized participant-scoped digest context.Default: "user-talks-first"

Response Fields

NameTypeDescription
id*
UUIDConversation ID
company_id*
UUIDTenant
project_id*
UUID | nullProject if set
type*
stringdm or group
channel*
stringChannel
status*
stringLifecycle status
title*
string | nullTitle
metadata*
object | nullMetadata
correlation_id*
UUID | nullCorrelation id if set
started_by*
UUID | nullCreator principal
subject_type*
string | nullLinked subject type
subject_id*
UUID | nullLinked subject id
from_channel_address_id*
UUID | nullOutbound channel address
bucket_id*
UUID | nullBacking attachment folder if any
privacy_level*
stringpublic or private (default private)
note_taking_cadence
object | nullnull inherits the company default cadence
starter_mode*
stringResolved conversation starter behavior
capabilities*
ConversationCapabilitiesPer-caller flags: view, send_message, edit, delete, manage_participants, manage_security, upload_attachment
participants*
ParticipantOut[]Participant rows
created_at*
datetimeCreated at
updated_at*
datetimeUpdated at
curl -X POST https://platform.ergondata.ai/api/v1/conversations/conversations \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{
    "company_id": "c0ffee00-cafe-babe-dead-beefcafebabe",
    "type": "dm",
    "channel": "direct",
    "participants": [
      {"principal_id": "u1111111-2222-3333-4444-555555555555", "principal_type": "user"},
      {"principal_id": "u2222222-3333-4444-5555-666666666666", "principal_type": "user"}
    ]
  }'

Response

201 Created
{
  "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "company_id": "c0ffee00-cafe-babe-dead-beefcafebabe",
  "project_id": null,
  "type": "dm",
  "channel": "direct",
  "status": "open",
  "title": null,
  "metadata": null,
  "correlation_id": null,
  "started_by": "u1111111-2222-3333-4444-555555555555",
  "subject_type": null,
  "subject_id": null,
  "from_channel_address_id": null,
  "bucket_id": null,
  "capabilities": {
    "view": true,
    "send_message": true,
    "edit": true,
    "delete": true,
    "manage_participants": true,
    "manage_security": false,
    "upload_attachment": true
  },
  "participants": [
    {
      "id": "p1000000-0000-4000-8000-000000000001",
      "conversation_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "principal_id": "u1111111-2222-3333-4444-555555555555",
      "principal_type": "user",
      "role": "member",
      "display_name": "Jane Doe",
      "avatar_url": null,
      "address": null,
      "joined_at": "2026-04-21T12:00:00Z",
      "left_at": null,
      "last_read_at": null
    },
    {
      "id": "p1000000-0000-4000-8000-000000000002",
      "conversation_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "principal_id": "u2222222-3333-4444-5555-666666666666",
      "principal_type": "user",
      "role": "member",
      "display_name": "John Roe",
      "avatar_url": null,
      "address": null,
      "joined_at": "2026-04-21T12:00:00Z",
      "left_at": null,
      "last_read_at": null
    }
  ],
  "created_at": "2026-04-21T12:00:00Z",
  "updated_at": "2026-04-21T12:00:00Z"
}
GET/api/v1/conversations/companies/{company_id}/conversations

List Conversations

Conversations the caller can view, newest activity first. Results are grant-filtered: each row is included only when the caller holds conversations:conversations:view on it (participation grants view, but an explicit security-page grant works too). Participant membership is a product filter layered on top, not the authorization source.

Bearer token required. Permission: conversations:conversations:view on each conversation resource

Path Parameters

NameTypeDescription
company_id*
string (UUID)Organization ID

Query Parameters

NameTypeDescription
limit
integerPage size (1–200)Default: 50
offset
integerOffsetDefault: 0
project_id
string (UUID) | nullFilter by project
channel
string | nullExact channel string
type
string | nulldm or group
status
string | nullStatus filter
participant_id
string (UUID) | nullMust include this principal
principal_type
string | nullFilter participant_id by principal type (user | agent | operator | contact)
q
string | nullCase-insensitive title substring

Response Fields

NameTypeDescription
items*
ConversationSummary[]Conversation summaries (no embedded messages)
total*
integerTotal matching rows
curl "https://platform.ergondata.ai/api/v1/conversations/companies/{company_id}/conversations?limit=20" \
  -H "Authorization: Bearer {token}"

Response

200 OK
{
  "items": [
    {
      "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "company_id": "c0ffee00-cafe-babe-dead-beefcafebabe",
      "project_id": null,
      "type": "dm",
      "channel": "direct",
      "status": "open",
      "title": null,
      "metadata": null,
      "started_by": "u1111111-2222-3333-4444-555555555555",
      "subject_type": null,
      "subject_id": null,
      "from_channel_address_id": null,
      "bucket_id": null,
      "capabilities": {
        "view": true,
        "send_message": true,
        "edit": false,
        "delete": false,
        "manage_participants": false,
        "manage_security": false,
        "upload_attachment": true
      },
      "participants": [],
      "created_at": "2026-04-21T12:00:00Z",
      "updated_at": "2026-04-21T12:05:00Z"
    }
  ],
  "total": 1
}
GET/api/v1/conversations/conversations/{conversation_id}

Get Conversation

Conversation metadata and participants. By default the messages array is empty (include_messages defaults to false) — load history through the paginated Messages endpoint (GET /conversations/{id}/messages) instead. Pass include_messages=true only for small threads where a single-shot load is acceptable. Requires view permission; reading can be granted explicitly without adding the viewer as a participant.

Bearer token required. Permission: conversations:conversations:view on the conversation

Path Parameters

NameTypeDescription
conversation_id*
string (UUID)Conversation ID

Query Parameters

NameTypeDescription
include_messages
booleanWhen true, eagerly loads the full message history into the messages array. Defaults to false; use the paginated Messages endpoint for real threads.Default: false

Response Fields

NameTypeDescription
messages*
MessageOut[]Messages in storage order. Empty unless include_messages=true was passed.
participants*
ParticipantOut[]Participant rows
auto_grant_enabled*
booleanWhether new participants are auto-granted view access
privacy_level*
"private" | "public"Cross-conversation visibility. private (default) means agents may only surface this thread into another conversation when every human participant here is also present there; public falls back to normal permission checks.
note_taking_cadence*
object | nullOptional cadence override (every_messages / every_minutes / every_tokens / min_messages). null inherits the company default.
(other fields)*
ConversationOut fieldsSame core fields as the create-conversation response (id, type, status, capabilities, subject_*, etc.)
curl https://platform.ergondata.ai/api/v1/conversations/conversations/{conversation_id} \
  -H "Authorization: Bearer {token}"

Response

200 OK
{
  "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "company_id": "c0ffee00-cafe-babe-dead-beefcafebabe",
  "project_id": null,
  "type": "dm",
  "channel": "direct",
  "status": "open",
  "title": null,
  "metadata": null,
  "correlation_id": null,
  "started_by": "u1111111-2222-3333-4444-555555555555",
  "subject_type": null,
  "subject_id": null,
  "from_channel_address_id": null,
  "bucket_id": null,
  "auto_grant_enabled": true,
  "privacy_level": "private",
  "note_taking_cadence": null,
  "capabilities": {
    "view": true,
    "send_message": true,
    "edit": true,
    "delete": true,
    "manage_participants": true,
    "manage_security": false,
    "upload_attachment": true
  },
  "participants": [],
  "messages": [],
  "created_at": "2026-04-21T12:00:00Z",
  "updated_at": "2026-04-21T12:01:00Z"
}
PATCH/api/v1/conversations/conversations/{conversation_id}

Update Conversation

Patch title, status, project_id, metadata, or auto_grant_enabled. Logs conversations.conversation.updated and publishes a stream envelope { type: conversation.updated, changes }.

Bearer token required. Permission: conversations:conversations:edit on the conversation

Path Parameters

NameTypeDescription
conversation_id*
string (UUID)Conversation ID

Request Body

NameTypeDescription
title
string | nullTitle
status
string | nullStatus
project_id
string (UUID) | nullProject
metadata
object | nullReplaces metadata when set. Use the reserved agent_model_override key (see Create Conversation) to pin which model an agent answers with for this conversation; PATCH it to switch the model mid-thread.
auto_grant_enabled
boolean | nullToggle auto-granting view access to participants
privacy_level
"private" | "public" | nullSet the conversation's cross-conversation visibility. private is the default; public opts the thread into normal permission checks for agent surfacing. Setting private is rejected (400) for all-agent conversations or any thread including a privacy-locked agent — those are always public.
note_taking_cadence
object | nullCadence override. Recognized keys: every_messages, every_minutes, every_tokens, min_messages (non-negative integers). Unknown keys are rejected; values are clamped to platform floors/ceilings by the worker. null inherits the company default.

Response Fields

NameTypeDescription
(ConversationOut)*
objectUpdated conversation, same fields as the create-conversation response (including privacy_level and note_taking_cadence)
curl -X PATCH https://platform.ergondata.ai/api/v1/conversations/conversations/{conversation_id} \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{"title": "Renewal — Acme Corp"}'

Response

200 OK
{
  "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "company_id": "c0ffee00-cafe-babe-dead-beefcafebabe",
  "project_id": null,
  "type": "group",
  "channel": "direct",
  "status": "open",
  "title": "Renewal — Acme Corp",
  "metadata": null,
  "correlation_id": null,
  "started_by": "u1111111-2222-3333-4444-555555555555",
  "subject_type": null,
  "subject_id": null,
  "from_channel_address_id": null,
  "bucket_id": null,
  "capabilities": {
    "view": true,
    "send_message": true,
    "edit": true,
    "delete": true,
    "manage_participants": true,
    "manage_security": false,
    "upload_attachment": true
  },
  "participants": [],
  "created_at": "2026-04-21T12:00:00Z",
  "updated_at": "2026-04-21T12:10:00Z"
}
DELETE/api/v1/conversations/conversations/{conversation_id}

Delete Conversation

Hard delete after conversations.conversation.deleted is logged.

Bearer token required. Permission: conversations:conversations:delete on the conversation

Path Parameters

NameTypeDescription
conversation_id*
string (UUID)Conversation ID
curl -X DELETE https://platform.ergondata.ai/api/v1/conversations/conversations/{conversation_id} \
  -H "Authorization: Bearer {token}"

Response

204 No Content
GET/api/v1/conversations/conversations

List Conversations by Subject

List conversations linked to a specific subject (e.g. a workflow item) so callers can discover threads bound to an entity before joining. Not participant-gated; cross-tenant exposure is prevented by the mandatory company_id filter.

Bearer token required. Permission: conversations:conversations:view (not participant-gated)

Query Parameters

NameTypeDescription
company_id*
string (UUID)Organization ID
subject_type*
stringSubject type (e.g. workflow.item)
subject_id*
string (UUID)Subject entity ID
limit
integerPage size (1–200)Default: 50
offset
integerOffsetDefault: 0

Response Fields

NameTypeDescription
items*
ConversationSummary[]Matching conversation summaries
total*
integerTotal count
curl "https://platform.ergondata.ai/api/v1/conversations/conversations?company_id={cid}&subject_type=workflow.item&subject_id={item_id}" \
  -H "Authorization: Bearer {token}"

Response

200 OK
{
  "items": [
    {
      "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "company_id": "c0ffee00-cafe-babe-dead-beefcafebabe",
      "project_id": null,
      "type": "dm",
      "channel": "direct",
      "status": "open",
      "title": null,
      "metadata": null,
      "started_by": "u1111111-2222-3333-4444-555555555555",
      "subject_type": "workflow.item",
      "subject_id": "a9e1c2b3-d4f5-6789-abcd-ef0123456789",
      "from_channel_address_id": null,
      "bucket_id": null,
      "participants": [],
      "created_at": "2026-04-20T10:00:00Z",
      "updated_at": "2026-04-20T14:30:00Z"
    }
  ],
  "total": 1
}
GET/api/v1/conversations/conversations/bulk-by-subject

Bulk List by Subject

Batch-resolve conversation summaries for multiple subjects in one call. Returns a map of subject_id to a summary with conversation_count and the distinct channels seen. Used by list UIs that need counts for many entities at once. Not participant-gated.

Bearer token required. Permission: conversations:conversations:view (not participant-gated)

Query Parameters

NameTypeDescription
company_id*
string (UUID)Organization ID
subject_type*
stringSubject type
subject_ids*
string (comma-separated UUIDs)Subject IDs to resolve

Response Fields

NameTypeDescription
by_subject_id*
objectMap of subject_id to SubjectConversationsSummary { conversation_count: integer, channels: string[] }
curl "https://platform.ergondata.ai/api/v1/conversations/conversations/bulk-by-subject?company_id={cid}&subject_type=workflow.item&subject_ids={id1},{id2}" \
  -H "Authorization: Bearer {token}"

Response

200 OK
{
  "by_subject_id": {
    "a9e1c2b3-d4f5-6789-abcd-ef0123456789": { "conversation_count": 2, "channels": ["direct", "email"] },
    "b8d2e3f4-a5c6-7890-bcde-f01234567890": { "conversation_count": 0, "channels": [] }
  }
}
POST/api/v1/conversations/conversations/bulk-delete

Bulk Delete Conversations

Delete multiple conversations at once. The delete permission is checked per id; ids the caller cannot delete land in forbidden and unknown ids in not_found instead of aborting the request. Permanently removes the conversations and their messages, participants, and attachment references.

Bearer token required. Permission: conversations:conversations:delete (checked per id)

Request Body

NameTypeDescription
conversation_ids*
string[] (UUID)Array of conversation IDs to delete

Response Fields

NameTypeDescription
deleted_ids*
string[]IDs that were deleted
forbidden*
string[]IDs the caller lacked permission to delete (defaults to [])
not_found*
string[]IDs that did not exist (defaults to [])
curl -X POST https://platform.ergondata.ai/api/v1/conversations/conversations/bulk-delete \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{"conversation_ids": ["f47ac10b-58cc-4372-a567-0e02b2c3d479", "a1b2c3d4-e5f6-7890-abcd-ef1234567890"]}'

Response

200 OK
{
  "deleted_ids": ["f47ac10b-58cc-4372-a567-0e02b2c3d479"],
  "forbidden": ["a1b2c3d4-e5f6-7890-abcd-ef1234567890"],
  "not_found": []
}

Messages

Append messages to a thread. Attachments must already exist as conversation-scoped Buckets files (upload via the attachment routes first). Tool, token, and content-block fields are stored for agent traces.

POST/api/v1/conversations/conversations/{conversation_id}/messages

Create Message

Post a message as an active participant. Defaults sender_participant_id to the caller's row; an explicit value must identify that same participant. This public route is also the conversations.messages.create automation capability, so automation principals receive the same participant, privacy, attachment, and channel reply checks. Emits conversations.message.added and streams message.created. Non-workflow Channel mode email requires channels:addresses:send; item-bound threads require workflows:items:reply_conversation.

Bearer token required. Permission: conversations:messages:create on the conversation; must be an active participant

Path Parameters

NameTypeDescription
conversation_id*
string (UUID)Conversation ID

Request Body

NameTypeDescription
content
string | nullPlain text body
role
stringMessage roleDefault: user
kind
stringMessage kindDefault: message
sender_participant_id
string (UUID) | nullDefaults to caller's participant id
content_blocks
array | nullStructured content blocks
attachments
AttachmentRef[] | nullAttachmentRef objects (file_id, filename, content_type, optional size, visibility, promoted_to_bucket_id, upload_batch_id, status, process_skip_reason)
tool_call_id
string | nullTool call correlation
tool_name
string | nullTool name
tool_args
object | nullSerialized arguments
tool_result
any | nullTool output
model_used
string | nullModel id
input_tokens
integer | nullPrompt tokens
output_tokens
integer | nullCompletion tokens
cost_usd
number | nullEstimated USD cost snapshot for the turn (tokens x model catalog price). Normally written by the agents runtime on the final assistant message.
channel_ref
object | nullExternal channel reference payload

Response Fields

NameTypeDescription
id*
UUIDMessage ID
conversation_id*
UUIDConversation
sender_participant_id*
UUID | nullSender participant row
role*
stringRole
kind*
stringMessage kind (default message)
content*
string | nullBody
content_blocks*
array | nullBlocks
attachments*
AttachmentRef[] | nullAttachment refs. Inbound email refs expose status and process_skip_reason and update over message.updated stream events
tool_call_id*
string | nullTool call id
tool_name*
string | nullTool name
tool_args*
object | nullTool args
tool_result*
any | nullTool result
model_used*
string | nullModel
input_tokens*
integer | nullIn tokens
output_tokens*
integer | nullOut tokens
cost_usd*
number | nullEstimated USD cost snapshot for the turn (tokens x model catalog price)
channel_ref*
object | nullChannel ref
origin*
object | nullCross-conversation provenance for relayed / cross-posted messages (server-set). null for ordinary in-thread messages.
created_at*
datetimeCreated at
curl -X POST https://platform.ergondata.ai/api/v1/conversations/conversations/{conversation_id}/messages \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{"content": "Please draft a renewal summary.", "role": "user"}'

Response

201 Created
{
  "id": "m3000000-0000-4000-8000-000000000003",
  "conversation_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "sender_participant_id": "p1000000-0000-4000-8000-000000000001",
  "role": "user",
  "kind": "message",
  "content": "Please draft a renewal summary.",
  "content_blocks": null,
  "attachments": null,
  "tool_call_id": null,
  "tool_name": null,
  "tool_args": null,
  "tool_result": null,
  "model_used": null,
  "input_tokens": null,
  "output_tokens": null,
  "cost_usd": null,
  "channel_ref": null,
  "origin": null,
  "created_at": "2026-04-21T12:15:00Z"
}
GET/api/v1/conversations/conversations/{conversation_id}/messages

List Messages

Keyset-paginated message history. Items are returned oldest-first within a page. Walk backwards through history with the before cursor (pass the previous page's oldest_cursor) and catch up on new messages with after (pass newest_cursor). This is the recommended way to load a thread; Get Conversation no longer returns messages by default.

Bearer token required. Permission: conversations:conversations:view on the conversation. Exposed to agents as the conversations.messages.list tool.

Path Parameters

NameTypeDescription
conversation_id*
string (UUID)Conversation ID

Query Parameters

NameTypeDescription
limit
integerPage size (1-200, clamped server-side).Default: 50
before
string (UUID)Return messages older than this message id (exclusive). Unknown cursor returns 404.
after
string (UUID)Return messages newer than this message id (exclusive). Unknown cursor returns 404.
since_ts
string (datetime)Inclusive lower time bound. Naive timestamps are treated as UTC.
until_ts
string (datetime)Inclusive upper time bound. Naive timestamps are treated as UTC.
include_tool_payloads
booleanWhen false, nulls out heavy tool_args/tool_result/content_blocks fields for a lighter scan.Default: true

Response Fields

NameTypeDescription
items*
MessageOut[]Messages for the page, oldest-first.
has_more*
booleanWhether more messages exist in the paging direction.
oldest_cursor*
string (UUID) | nullMessage id of the oldest item; pass as before to page older.
newest_cursor*
string (UUID) | nullMessage id of the newest item; pass as after to page newer.
curl "https://platform.ergondata.ai/api/v1/conversations/conversations/{conversation_id}/messages?limit=50" \
  -H "Authorization: Bearer {token}"

Response

200 OK
{
  "items": [
    {
      "id": "m3000000-0000-4000-8000-000000000003",
      "conversation_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "sender_participant_id": "p1000000-0000-4000-8000-000000000001",
      "role": "user",
      "kind": "message",
      "content": "Hello!",
      "content_blocks": null,
      "attachments": null,
      "tool_call_id": null,
      "tool_name": null,
      "tool_args": null,
      "tool_result": null,
      "model_used": null,
      "input_tokens": null,
      "output_tokens": null,
      "cost_usd": null,
      "channel_ref": null,
      "created_at": "2026-04-21T12:01:00Z"
    }
  ],
  "has_more": false,
  "oldest_cursor": "m3000000-0000-4000-8000-000000000003",
  "newest_cursor": "m3000000-0000-4000-8000-000000000003"
}

Notes

Whenever an agent participates, the note-taker summarizes bounded new windows into granular notes (topic, what happened, decisions, open questions, entities) and folds eligible notes into one living digest. Close events and the idle settlement sweeper capture the final remainder. Granular Markdown is exported to an embedding-only notes/ folder and backs conversations.notes.search; the authoritative rows and living digest remain in Conversations.

GET/api/v1/conversations/conversations/{conversation_id}/notes

Search Notes

Keyset-paginated notes for a conversation with hybrid lexical/semantic ranking and optional time-window filters. Items are ordered deterministically. Exposed to agents as conversations.notes.search; cross-conversation surfacing is subject to the conversation's privacy_level.

Bearer token required. Permission: conversations:conversations:view OR the granular conversations:notes:view on the conversation (notes:view lets a principal read notes without full thread access). For public conversations, agents:conversations:view on a participating agent also grants derived read.

Path Parameters

NameTypeDescription
conversation_id*
string (UUID)Conversation ID

Query Parameters

NameTypeDescription
q
stringHybrid lexical/semantic query across topic, what_happened, decisions, open_questions, and entities.
limit
integerPage size (1-200, clamped server-side).Default: 50
before
string (UUID)Return notes older than this note id (exclusive). Unknown cursor returns 404.
after
string (UUID)Return notes newer than this note id (exclusive). Unknown cursor returns 404.
since_ts
string (datetime)Only notes whose covered window overlaps at/after this time (falls back to created_at when window bounds are null).
until_ts
string (datetime)Only notes whose covered window overlaps at/before this time.

Response Fields

NameTypeDescription
items*
NoteOut[]Notes for the page. Each note has topic, what_happened, decisions[], open_questions[], entities[], window_start, window_end, message_count, and optional bucket_id/file_id pointers.
has_more*
booleanWhether more notes exist in the paging direction.
oldest_cursor*
string (UUID) | nullNote id of the oldest item; pass as before to page older.
newest_cursor*
string (UUID) | nullNote id of the newest item; pass as after to page newer.
curl "https://platform.ergondata.ai/api/v1/conversations/conversations/{conversation_id}/notes?q=billing&limit=20" \
  -H "Authorization: Bearer {token}"

Response

200 OK
{
  "items": [
    {
      "id": "n1000000-0000-4000-8000-000000000001",
      "conversation_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "topic": "Renewal billing terms",
      "what_happened": "Customer asked about proration on the annual renewal; agent confirmed mid-term upgrades are prorated.",
      "decisions": ["Send renewal quote with prorated line items"],
      "open_questions": ["Confirm PO number before invoicing"],
      "entities": ["Acme Corp", "Annual Plan"],
      "window_start": "2026-04-21T12:00:00Z",
      "window_end": "2026-04-21T12:20:00Z",
      "message_count": 14,
      "bucket_id": "b1000000-0000-4000-8000-000000000001",
      "file_id": "f1000000-0000-4000-8000-000000000001",
      "created_at": "2026-04-21T12:20:05Z"
    }
  ],
  "has_more": false,
  "oldest_cursor": "n1000000-0000-4000-8000-000000000001",
  "newest_cursor": "n1000000-0000-4000-8000-000000000001"
}

Participants

Membership changes and read-position updates. Adding a third participant to a DM upgrades it to a group (auto-title if missing). Removing sets left_at instead of deleting history. Participant rows are enriched with display_name, avatar_url, and (for contacts) address.

POST/api/v1/conversations/conversations/{conversation_id}/participants

Add Participant

Invite a principal. 409 when they are already active. Emits conversations.participant.added and streams participant.added.

Bearer token required. Permission: conversations:participants:manage on the conversation

Path Parameters

NameTypeDescription
conversation_id*
string (UUID)Conversation ID

Request Body

NameTypeDescription
principal_id*
string (UUID)Principal to add
principal_type*
stringuser | agent | operator | contact
role
stringMembership roleDefault: member

Response Fields

NameTypeDescription
(ParticipantOut)*
objectParticipant row: id, conversation_id, principal_id, principal_type, role, display_name, avatar_url, address, joined_at, left_at, last_read_at
curl -X POST https://platform.ergondata.ai/api/v1/conversations/conversations/{conversation_id}/participants \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{
    "principal_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
    "principal_type": "agent"
  }'

Response

201 Created
{
  "id": "p2000000-0000-4000-8000-000000000002",
  "conversation_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "principal_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
  "principal_type": "agent",
  "role": "member",
  "display_name": "Support Agent",
  "avatar_url": null,
  "address": null,
  "joined_at": "2026-04-21T12:20:00Z",
  "left_at": null,
  "last_read_at": null
}
PATCH/api/v1/conversations/conversations/{conversation_id}/participants/{participant_id}

Update Participant

Currently supports advancing last_read_at for read receipts. Requires conversation view permission plus active membership.

Bearer token required. Permission: conversations:conversations:view; must be an active participant

Path Parameters

NameTypeDescription
conversation_id*
string (UUID)Conversation ID
participant_id*
string (UUID)Participant row ID

Request Body

NameTypeDescription
last_read_at
string (ISO 8601 datetime) | nullRead cursor timestamp

Response Fields

NameTypeDescription
(ParticipantOut)*
objectUpdated participant row
curl -X PATCH https://platform.ergondata.ai/api/v1/conversations/conversations/{conversation_id}/participants/{participant_id} \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{"last_read_at": "2026-04-21T12:25:00+00:00"}'

Response

200 OK
{
  "id": "p1000000-0000-4000-8000-000000000001",
  "conversation_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "principal_id": "u1111111-2222-3333-4444-555555555555",
  "principal_type": "user",
  "role": "member",
  "display_name": "Jane Doe",
  "avatar_url": null,
  "address": null,
  "joined_at": "2026-04-21T12:00:00Z",
  "left_at": null,
  "last_read_at": "2026-04-21T12:25:00Z"
}
DELETE/api/v1/conversations/conversations/{conversation_id}/participants/{participant_id}

Remove Participant

Soft-remove by setting left_at. Emits conversations.participant.removed and streams participant.removed.

Bearer token required. Permission: conversations:participants:manage on the conversation

Path Parameters

NameTypeDescription
conversation_id*
string (UUID)Conversation ID
participant_id*
string (UUID)Participant row ID
curl -X DELETE https://platform.ergondata.ai/api/v1/conversations/conversations/{conversation_id}/participants/{participant_id} \
  -H "Authorization: Bearer {token}"

Response

204 No Content

Attachments

Broker uploads to Buckets as conversation-scoped files (the service stores references only). Optional X-Correlation-ID header is forwarded. Oversize files return 413; Buckets failures return 502 with a logged conversations.attachment.failed event. The returned AttachmentRef is embedded in the next message.

POST/api/v1/conversations/conversations/{conversation_id}/attachments

Upload Attachment

Multipart upload with a single file field. Returns an AttachmentRef to embed in the next message. Requires attachment permission and active membership.

Bearer token required. Permission: conversations:attachments:create on the conversation; must be an active participant

Path Parameters

NameTypeDescription
conversation_id*
string (UUID)Conversation ID

Request Body

NameTypeDescription
file*
file (multipart)Binary payload (field name file)

Response Fields

NameTypeDescription
file_id*
UUIDBuckets file id
filename*
stringFilename
content_type*
stringMIME type
size*
integer | nullByte length
visibility*
stringBuckets visibility (default bucket)
promoted_to_bucket_id*
UUID | nullFolder promotion if any
upload_batch_id*
UUID | nullBatch id when applicable
status*
string | nullInbound import or Buckets state: pending, downloading, uploading, processing, skipped, oversized, or failed
process_skip_reason*
string | nullReason an attachment was skipped, oversized, retrying, or failed
curl -X POST https://platform.ergondata.ai/api/v1/conversations/conversations/{conversation_id}/attachments \
  -H "Authorization: Bearer {token}" \
  -H "X-Correlation-ID: corr-7f3c9a2b-4d1e-4c88-9aa1-001122334455" \
  -F "file=@./brief.pdf;type=application/pdf"

Response

201 Created
{
  "file_id": "d4000000-0000-4000-8000-000000000004",
  "filename": "brief.pdf",
  "content_type": "application/pdf",
  "size": 524288,
  "visibility": "bucket",
  "promoted_to_bucket_id": null,
  "upload_batch_id": null,
  "status": "processing",
  "process_skip_reason": null
}
POST/api/v1/conversations/conversations/{conversation_id}/attachments/batch

Upload Attachments (Batch)

Multipart request with repeated files field for N attachments. Accepted files share upload_batch_id; skipped files list Buckets decisions without failing the whole batch.

Bearer token required. Permission: conversations:attachments:create on the conversation; must be an active participant

Path Parameters

NameTypeDescription
conversation_id*
string (UUID)Conversation ID

Request Body

NameTypeDescription
files*
file[] (multipart)One or more parts named files

Response Fields

NameTypeDescription
upload_batch_id*
UUIDShared batch id
attachments*
AttachmentRef[]AttachmentRef for each accepted file
skipped*
AttachmentSkipped[]Skipped rows (file_id, decision, reason). Defaults to []
curl -X POST https://platform.ergondata.ai/api/v1/conversations/conversations/{conversation_id}/attachments/batch \
  -H "Authorization: Bearer {token}" \
  -F "files=@./page1.png;type=image/png" \
  -F "files=@./notes.txt;type=text/plain"

Response

201 Created
{
  "upload_batch_id": "b5000000-0000-4000-8000-000000000005",
  "attachments": [
    {
      "file_id": "d4000000-0000-4000-8000-000000000004",
      "filename": "page1.png",
      "content_type": "image/png",
      "size": 98234,
      "visibility": "bucket",
      "promoted_to_bucket_id": null,
      "upload_batch_id": "b5000000-0000-4000-8000-000000000005"
    }
  ],
  "skipped": [
    {
      "file_id": "d4999999-9999-4999-8999-999999999999",
      "decision": "rejected",
      "reason": "unsupported_mime"
    }
  ]
}

Settings

Company-scoped note-taking cadence (the compute-egress pattern): service-owned config surfaced under IAM → Settings. Notes run automatically whenever an agent participates; note_taking_cadence controls the company windowing policy, with empty keys falling back to built-in defaults. Company context comes from the token (or the X-Company-Id header).

GET/api/v1/conversations/settings/note-taking

Get Company Note-Taking Settings

Return both the company's sparse note-taking cadence overrides and the fully resolved effective cadence. Automatic notes are on for every conversation with an active agent; the platform-managed feature flag is an operational kill switch, not a tenant opt-in.

Bearer token required. Permission: conversations:settings:view on org/{company_id}

Response Fields

NameTypeDescription
company_id*
string (UUID)Resolved company id
note_taking_cadence*
objectCompany cadence policy. Keys: every_messages, every_minutes, every_tokens, min_messages. Empty keys fall back to built-in defaults.
effective_note_taking_cadence*
objectFully resolved every_messages, every_minutes, every_tokens, and min_messages values after applying platform defaults. Use these values for placeholders and previews.
curl https://platform.ergondata.ai/api/v1/conversations/settings/note-taking \
  -H "Authorization: Bearer {token}"

Response

200 OK
{
  "company_id": "c0ffee00-cafe-babe-dead-beefcafebabe",
  "note_taking_cadence": {
    "every_messages": 20,
    "min_messages": 4
  },
  "effective_note_taking_cadence": {
    "every_messages": 20,
    "every_minutes": 15,
    "every_tokens": 4000,
    "min_messages": 4
  }
}
PUT/api/v1/conversations/settings/note-taking

Update Company Note-Taking Settings

Upsert the company's note-taking cadence. Unknown cadence keys are rejected; values are validated as non-negative integers.

Bearer token required. Permission: conversations:settings:manage on org/{company_id}

Request Body

NameTypeDescription
note_taking_cadence*
objectCadence policy (every_messages, every_minutes, every_tokens, min_messages).

Response Fields

NameTypeDescription
(CompanyNoteSettingsOut)*
objectThe stored settings, same shape as the GET response.
curl -X PUT https://platform.ergondata.ai/api/v1/conversations/settings/note-taking \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{
    "note_taking_cadence": { "every_messages": 20, "min_messages": 4 }
  }'

Response

200 OK
{
  "company_id": "c0ffee00-cafe-babe-dead-beefcafebabe",
  "note_taking_cadence": { "every_messages": 20, "min_messages": 4 },
  "effective_note_taking_cadence": { "every_messages": 20, "every_minutes": 15, "every_tokens": 4000, "min_messages": 4 }
}
GET/api/v1/conversations/settings/a2a

Get Agent-to-Agent Settings

Return the company's agent-to-agent (A2A) messaging policy: whether bounded relay of agent-sent messages to other agent participants is enabled, plus the loop-safety budgets. Values are already clamped to platform limits; defaults are returned when no row exists. A2A is off by default.

Bearer token required. Permission: conversations:settings:view on org/{company_id}

Response Fields

NameTypeDescription
company_id*
UUIDCompany
enabled*
booleanWhether A2A relay is enabled (default false).
max_consecutive_agent_turns*
integerTrailing agent-turn budget before the exchange pauses (clamped to the platform cap).
agent_messages_per_window*
integerAgent messages allowed per rolling window (clamped to the platform cap).
window_seconds*
integerRolling window (seconds) for the rate budget. Platform constant.
curl https://platform.ergondata.ai/api/v1/conversations/settings/a2a \
  -H "Authorization: Bearer {token}"

Response

200 OK
{
  "company_id": "c0ffee00-cafe-babe-dead-beefcafebabe",
  "enabled": false,
  "max_consecutive_agent_turns": 6,
  "agent_messages_per_window": 30,
  "window_seconds": 600
}
PUT/api/v1/conversations/settings/a2a

Update Agent-to-Agent Settings

Set the company's A2A messaging policy. When enabled, an agent-sent message can be relayed to the other agent participants of a thread, bounded by the budgets. Requested budgets above the platform hard caps are clamped — the response reflects the stored (clamped) values. Default is off.

Bearer token required. Permission: conversations:settings:manage on org/{company_id}

Request Body

NameTypeDescription
enabled
booleanTurn A2A relay on/off (default false).
max_consecutive_agent_turns
integer | nullTrailing agent-turn budget. null uses the platform default; values above the cap are clamped.
agent_messages_per_window
integer | nullAgent messages per window. null uses the default; values above the cap are clamped.

Response Fields

NameTypeDescription
(CompanyA2ASettingsOut)*
objectThe stored, clamped settings, same shape as the GET response.
curl -X PUT https://platform.ergondata.ai/api/v1/conversations/settings/a2a \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{ "enabled": true, "max_consecutive_agent_turns": 6 }'

Response

200 OK
{
  "company_id": "c0ffee00-cafe-babe-dead-beefcafebabe",
  "enabled": true,
  "max_consecutive_agent_turns": 6,
  "agent_messages_per_window": 30,
  "window_seconds": 600
}

Access Management

Project and conversation security-page endpoints. They proxy IAM grants inside the selected resource boundary and require `conversations:permissions:manage` on that project or conversation. Each boundary exposes eligible principals, the grantable resource-type/permission tree, and a grants collection.

POST/api/v1/conversations/access/conversation-projects/grants/batch

Create Project Access Grants Batch

Create grants across conversation-project roots with ordered, independent outcomes. The grouped operations expand resources first and permission IDs second, to at most 200 grants total. Validation failures do not block valid items. The response remains 200 for partial success; retry failed items or safely retry the whole request—existing natural grant tuples return already_exists. A side_effect_error reports failed post-write reconciliation, not a failed grant. ToolDef slug: conversations.access_grants.create_project_grants_batch.

Bearer token required. Permission: conversations:permissions:manage on every target conversation-project root.

Request Body

NameTypeDescription
operations*
array1–200 grouped operations; resources × permission_ids across all operations must expand to at most 200 grants

Response Fields

NameTypeDescription
results*
arrayOne result per expanded grant, ordered by operation, then resource, then permission
summary*
objectOutcome counts
curl -X POST https://platform.ergondata.ai/api/v1/conversations/access/conversation-projects/grants/batch \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{"operations":[{"client_ref":"reviewers","principal_type":"member","principal_id":"{principal_id}","resources":["org/{company_id}/conversation-project/{project_id}"],"permission_ids":["{permission_id}"],"effect":"allow"}]}'

Response

200 OK
{
  "results": [{
    "index": 0,
    "client_ref": "reviewers",
    "status": "created",
    "principal_type": "member",
    "principal_id": "{principal_id}",
    "permission_id": "{permission_id}",
    "resource": "org/{company_id}/conversation-project/{project_id}",
    "effect": "allow",
    "grant": {"id": "{grant_id}", "permission_id": "{permission_id}", "name": "conversations:projects:view", "resource": "org/{company_id}/conversation-project/{project_id}", "effect": "allow", "is_system": false, "granted_at": "2026-07-15T16:30:00Z"},
    "error_status": null,
    "error_detail": null,
    "side_effect_error_status": null,
    "side_effect_error_detail": null
  }],
  "summary": {"created": 1, "already_exists": 0, "failed": 0}
}
POST/api/v1/conversations/access/conversations/grants/batch

Create Conversation Access Grants Batch

Create grants across conversation roots with the same ordered BatchCreateGrantsRequest contract and 200-expanded-grant limit. Items succeed or fail independently and partial success returns 200. Whole-request retries are safe: existing natural grant tuples return already_exists; retry side_effect_error outcomes to reconcile post-write work because their grants already exist. ToolDef slug: conversations.access_grants.create_conversation_grants_batch.

Bearer token required. Permission: conversations:permissions:manage on every target conversation root.

Request Body

NameTypeDescription
operations*
array1–200 grouped operations; resources × permission_ids across all operations must expand to at most 200 grants

Response Fields

NameTypeDescription
results*
arrayOne result per expanded grant, ordered by operation, then resource, then permission
summary*
objectOutcome counts
curl -X POST https://platform.ergondata.ai/api/v1/conversations/access/conversations/grants/batch \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{"operations":[{"principal_type":"agent","principal_id":"{principal_id}","resources":["org/{company_id}/conversation/{conversation_id}"],"permission_ids":["{permission_id}"]}]}'

Response

200 OK
{
  "results": [{
    "index": 0,
    "client_ref": null,
    "status": "already_exists",
    "principal_type": "agent",
    "principal_id": "{principal_id}",
    "permission_id": "{permission_id}",
    "resource": "org/{company_id}/conversation/{conversation_id}",
    "effect": "allow",
    "grant": {"id": "{grant_id}", "permission_id": "{permission_id}", "name": "conversations:messages:create", "resource": "org/{company_id}/conversation/{conversation_id}", "effect": "allow", "is_system": false, "granted_at": "2026-07-15T16:30:00Z"},
    "error_status": null,
    "error_detail": null,
    "side_effect_error_status": null,
    "side_effect_error_detail": null
  }],
  "summary": {"created": 0, "already_exists": 1, "failed": 0}
}
GET/api/v1/conversations/projects/{project_id}/access/eligible

List Project Eligible Principals

Return IAM principals that can receive Conversations grants on this project.

Bearer token required. Permission: conversations:permissions:manage on org/{company_id}/conversation-project/{project_id}.

Path Parameters

NameTypeDescription
project_id*
string (UUID)Project ID

Response Fields

NameTypeDescription
[]*
EligiblePrincipal[]Eligible principals with principal_type, principal_id, and label
curl https://platform.ergondata.ai/api/v1/conversations/projects/{project_id}/access/eligible \
  -H "Authorization: Bearer {token}"

Response

200 OK
[
  {
    "principal_type": "member",
    "principal_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "label": "Jane Doe"
  },
  {
    "principal_type": "agent",
    "principal_id": "d4e5f6a7-b8c9-0123-def0-456789012345",
    "label": "Support Agent"
  }
]
GET/api/v1/conversations/projects/{project_id}/access/resource-types

List Project Grantable Permissions

Return Conversations resource types and permissions grantable from the project boundary. Project access pages exclude top-level create permissions and conversation-thread permissions.

Bearer token required. Permission: conversations:permissions:manage on org/{company_id}/conversation-project/{project_id}.

Path Parameters

NameTypeDescription
project_id*
string (UUID)Project ID

Response Fields

NameTypeDescription
resource_types*
ResourceTypeNode[]Conversations resource type tree
resource_type_edges*
ResourceTypeEdgeNode[]Resource type parent/child edges (defaults to [])
permissions*
PermissionOption[]Permissions grantable from this project boundary
curl https://platform.ergondata.ai/api/v1/conversations/projects/{project_id}/access/resource-types \
  -H "Authorization: Bearer {token}"

Response

200 OK
{
  "resource_types": [
    { "id": "rt-project", "name": "conversation-project", "slug": "conversation-project", "parent_id": null, "children": [] }
  ],
  "resource_type_edges": [],
  "permissions": [
    {
      "id": "perm-project-view",
      "name": "conversations:projects:view",
      "friendly_name": "View conversation projects",
      "description": "View this conversation project",
      "resource_type_id": "rt-project",
      "parent_resource_type_slug": null,
      "scope_anchor": "instance",
      "display_order": 10
    }
  ]
}
GET/api/v1/conversations/projects/{project_id}/access/grants

List Project Access Grants

List IAM grants scoped to this conversation project boundary.

Bearer token required. Permission: conversations:permissions:manage on org/{company_id}/conversation-project/{project_id}.

Path Parameters

NameTypeDescription
project_id*
string (UUID)Project ID

Query Parameters

NameTypeDescription
page
integerPage numberDefault: 1
limit
integerPage size (1-500)Default: 100

Response Fields

NameTypeDescription
items*
GrantResponse[]Grants in this page
total*
integerTotal grants
page*
integerCurrent page
limit*
integerPage size
curl "https://platform.ergondata.ai/api/v1/conversations/projects/{project_id}/access/grants?page=1&limit=100" \
  -H "Authorization: Bearer {token}"

Response

200 OK
{
  "items": [
    {
      "id": "grant-001",
      "permission_id": "perm-project-view",
      "name": "conversations:projects:view",
      "resource": "org/c0ffee00-cafe-babe-dead-beefcafebabe/conversation-project/b2c3d4e5-f6a7-8901-bcde-f12345678901",
      "effect": "allow",
      "is_system": false,
      "granted_at": "2026-05-17T12:00:00Z"
    }
  ],
  "total": 1,
  "page": 1,
  "limit": 100
}
POST/api/v1/conversations/projects/{project_id}/access/grants

Create Project Access Grant

Grant a project-scoped Conversations permission to a principal. The optional resource override must stay within the project resource prefix.

Bearer token required. Permission: conversations:permissions:manage on org/{company_id}/conversation-project/{project_id}.

Path Parameters

NameTypeDescription
project_id*
string (UUID)Project ID

Request Body

NameTypeDescription
principal_type*
stringIAM principal type (member | api_key | agent | automation). Distinct from conversation participant types such as user/operator/contact.
principal_id*
stringIAM principal UUID
permission_id*
stringIAM permission UUID
resource
string | nullDefaults to the project boundary; must stay within it
effect
stringGrant effect (allow | deny)Default: allow

Response Fields

NameTypeDescription
id*
stringGrant ID
permission_id*
stringPermission ID
name*
stringPermission slug
resource*
stringResource path
effect*
stringGrant effect
is_system*
booleanWhether IAM marked the grant as system-managed
granted_at*
datetimeCreation time
curl -X POST https://platform.ergondata.ai/api/v1/conversations/projects/{project_id}/access/grants \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{
    "principal_type": "member",
    "principal_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "permission_id": "perm-project-view"
  }'

Response

201 Created
{
  "id": "grant-001",
  "permission_id": "perm-project-view",
  "name": "conversations:projects:view",
  "resource": "org/c0ffee00-cafe-babe-dead-beefcafebabe/conversation-project/b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "effect": "allow",
  "is_system": false,
  "granted_at": "2026-05-17T12:00:00Z"
}
DELETE/api/v1/conversations/projects/{project_id}/access/grants/{grant_id}

Delete Project Access Grant

Revoke an IAM grant through the project security page.

Bearer token required. Permission: conversations:permissions:manage on org/{company_id}/conversation-project/{project_id}.

Path Parameters

NameTypeDescription
project_id*
string (UUID)Project ID
grant_id*
stringGrant ID
curl -X DELETE https://platform.ergondata.ai/api/v1/conversations/projects/{project_id}/access/grants/{grant_id} \
  -H "Authorization: Bearer {token}"

Response

204 No Content
GET/api/v1/conversations/conversations/{conversation_id}/access/eligible

List Conversation Eligible Principals

Return IAM principals that can receive Conversations grants on this conversation.

Bearer token required. Permission: conversations:permissions:manage on org/{company_id}/conversation/{conversation_id}.

Path Parameters

NameTypeDescription
conversation_id*
string (UUID)Conversation ID

Response Fields

NameTypeDescription
[]*
EligiblePrincipal[]Eligible principals with principal_type, principal_id, and label
curl https://platform.ergondata.ai/api/v1/conversations/conversations/{conversation_id}/access/eligible \
  -H "Authorization: Bearer {token}"

Response

200 OK
[
  {
    "principal_type": "member",
    "principal_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "label": "Jane Doe"
  }
]
GET/api/v1/conversations/conversations/{conversation_id}/access/resource-types

List Conversation Grantable Permissions

Return Conversations resource types and permissions grantable from the conversation boundary (conversation, message, participant, attachment, and permissions-management permissions).

Bearer token required. Permission: conversations:permissions:manage on org/{company_id}/conversation/{conversation_id}.

Path Parameters

NameTypeDescription
conversation_id*
string (UUID)Conversation ID

Response Fields

NameTypeDescription
resource_types*
ResourceTypeNode[]Conversations resource type tree
resource_type_edges*
ResourceTypeEdgeNode[]Resource type parent/child edges (defaults to [])
permissions*
PermissionOption[]Permissions grantable from this conversation boundary
curl https://platform.ergondata.ai/api/v1/conversations/conversations/{conversation_id}/access/resource-types \
  -H "Authorization: Bearer {token}"

Response

200 OK
{
  "resource_types": [
    { "id": "rt-conversation", "name": "conversation", "slug": "conversation", "parent_id": null, "children": [] }
  ],
  "resource_type_edges": [],
  "permissions": [
    {
      "id": "perm-message-create",
      "name": "conversations:messages:create",
      "friendly_name": "Post messages",
      "description": "Post messages in this conversation",
      "resource_type_id": "rt-conversation",
      "parent_resource_type_slug": null,
      "scope_anchor": "instance",
      "display_order": 20
    }
  ]
}
GET/api/v1/conversations/conversations/{conversation_id}/access/grants

List Conversation Access Grants

List IAM grants scoped directly to one conversation.

Bearer token required. Permission: conversations:permissions:manage on org/{company_id}/conversation/{conversation_id}.

Path Parameters

NameTypeDescription
conversation_id*
string (UUID)Conversation ID

Query Parameters

NameTypeDescription
page
integerPage numberDefault: 1
limit
integerPage size (1-500)Default: 100

Response Fields

NameTypeDescription
items*
GrantResponse[]Grants in this page
total*
integerTotal grants
page*
integerCurrent page
limit*
integerPage size
curl "https://platform.ergondata.ai/api/v1/conversations/conversations/{conversation_id}/access/grants?page=1&limit=100" \
  -H "Authorization: Bearer {token}"

Response

200 OK
{
  "items": [],
  "total": 0,
  "page": 1,
  "limit": 100
}
POST/api/v1/conversations/conversations/{conversation_id}/access/grants

Create Conversation Access Grant

Grant a conversation-local permission. Conversation pages accept conversation, message, participant, attachment, and permissions-management grants; project permissions are rejected.

Bearer token required. Permission: conversations:permissions:manage on org/{company_id}/conversation/{conversation_id}.

Path Parameters

NameTypeDescription
conversation_id*
string (UUID)Conversation ID

Request Body

NameTypeDescription
principal_type*
stringIAM principal type (member | api_key | agent | automation)
principal_id*
stringIAM principal UUID
permission_id*
stringIAM permission UUID
resource
string | nullDefaults to the conversation boundary; must stay within it
effect
stringGrant effect (allow | deny)Default: allow

Response Fields

NameTypeDescription
id*
stringGrant ID
permission_id*
stringPermission ID
name*
stringPermission slug
resource*
stringResource path
effect*
stringGrant effect
is_system*
booleanWhether IAM marked the grant as system-managed
granted_at*
datetimeCreation time
curl -X POST https://platform.ergondata.ai/api/v1/conversations/conversations/{conversation_id}/access/grants \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{
    "principal_type": "agent",
    "principal_id": "d4e5f6a7-b8c9-0123-def0-456789012345",
    "permission_id": "perm-message-create"
  }'

Response

201 Created
{
  "id": "grant-002",
  "permission_id": "perm-message-create",
  "name": "conversations:messages:create",
  "resource": "org/c0ffee00-cafe-babe-dead-beefcafebabe/conversation/f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "effect": "allow",
  "is_system": false,
  "granted_at": "2026-05-17T12:05:00Z"
}
DELETE/api/v1/conversations/conversations/{conversation_id}/access/grants/{grant_id}

Delete Conversation Access Grant

Revoke an IAM grant through the conversation security page.

Bearer token required. Permission: conversations:permissions:manage on org/{company_id}/conversation/{conversation_id}.

Path Parameters

NameTypeDescription
conversation_id*
string (UUID)Conversation ID
grant_id*
stringGrant ID
curl -X DELETE https://platform.ergondata.ai/api/v1/conversations/conversations/{conversation_id}/access/grants/{grant_id} \
  -H "Authorization: Bearer {token}"

Response

204 No Content

Activity

Durable, append-only audit log of conversation and project lifecycle events. Each project and conversation exposes a paginated feed (filterable by event_type and correlation_id), and a single event can be fetched by id. Events carry actor, correlation, and event-specific payload data.

GET/api/v1/conversations/conversations/{conversation_id}/activity

List Conversation Activity

Paginated activity events for one conversation, newest first. Optionally filter by event_type and correlation_id.

Bearer token required. Permission: conversations:activity:view on the conversation

Path Parameters

NameTypeDescription
conversation_id*
string (UUID)Conversation ID

Query Parameters

NameTypeDescription
event_type
string | nullExact event slug filter (e.g. conversations.message.added)
correlation_id
string (UUID) | nullFilter by correlation id
page
integerPage number (>= 1)Default: 1
limit
integerPage size (1-100)Default: 50

Response Fields

NameTypeDescription
items*
ActivityEventItem[]Events: id, event_type, conversation_id, project_id, actor_id, actor_type, correlation_id, data, created_at
total*
integerTotal matching events
page*
integerCurrent page
limit*
integerPage size
curl "https://platform.ergondata.ai/api/v1/conversations/conversations/{conversation_id}/activity?page=1&limit=50" \
  -H "Authorization: Bearer {token}"

Response

200 OK
{
  "items": [
    {
      "id": "e1a2b3c4-d5e6-7890-abcd-ef0123456789",
      "event_type": "conversations.message.added",
      "conversation_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "project_id": null,
      "actor_id": "u1111111-2222-3333-4444-555555555555",
      "actor_type": "user",
      "correlation_id": null,
      "data": { "message_id": "m3000000-0000-4000-8000-000000000003" },
      "created_at": "2026-04-21T12:15:00Z"
    }
  ],
  "total": 1,
  "page": 1,
  "limit": 50
}
GET/api/v1/conversations/projects/{project_id}/activity

List Project Activity

Paginated activity events for one project, newest first. Optionally filter by event_type and correlation_id.

Bearer token required. Permission: conversations:activity:view on the project

Path Parameters

NameTypeDescription
project_id*
string (UUID)Project ID

Query Parameters

NameTypeDescription
event_type
string | nullExact event slug filter (e.g. conversations.project.updated)
correlation_id
string (UUID) | nullFilter by correlation id
page
integerPage number (>= 1)Default: 1
limit
integerPage size (1-100)Default: 50

Response Fields

NameTypeDescription
items*
ActivityEventItem[]Activity events scoped to the project
total*
integerTotal matching events
page*
integerCurrent page
limit*
integerPage size
curl "https://platform.ergondata.ai/api/v1/conversations/projects/{project_id}/activity?page=1&limit=50" \
  -H "Authorization: Bearer {token}"

Response

200 OK
{
  "items": [
    {
      "id": "e2b3c4d5-e6f7-8901-bcde-f01234567890",
      "event_type": "conversations.project.updated",
      "conversation_id": null,
      "project_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
      "actor_id": "u1111111-2222-3333-4444-555555555555",
      "actor_type": "user",
      "correlation_id": null,
      "data": { "changes": { "name": "Customer Success (EMEA)" } },
      "created_at": "2026-04-21T11:30:00Z"
    }
  ],
  "total": 1,
  "page": 1,
  "limit": 50
}
GET/api/v1/conversations/activity/{event_id}

Get Activity Event

Fetch a single activity event by id. Authorization is resolved against the event's conversation, project, or org resource.

Bearer token required. Permission: conversations:activity:view on the event's conversation/project/org resource

Path Parameters

NameTypeDescription
event_id*
string (UUID)Activity event ID

Response Fields

NameTypeDescription
id*
UUIDEvent ID
event_type*
stringEvent slug
conversation_id*
UUID | nullConversation the event belongs to
project_id*
UUID | nullProject the event belongs to
actor_id*
UUID | nullActor principal
actor_type*
string | nullActor principal type
correlation_id*
UUID | nullCorrelation id if set
data*
object | nullEvent-specific payload
created_at*
datetimeCreated at
curl https://platform.ergondata.ai/api/v1/conversations/activity/{event_id} \
  -H "Authorization: Bearer {token}"

Response

200 OK
{
  "id": "e1a2b3c4-d5e6-7890-abcd-ef0123456789",
  "event_type": "conversations.message.added",
  "conversation_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "project_id": null,
  "actor_id": "u1111111-2222-3333-4444-555555555555",
  "actor_type": "user",
  "correlation_id": null,
  "data": { "message_id": "m3000000-0000-4000-8000-000000000003" },
  "created_at": "2026-04-21T12:15:00Z"
}

Real-Time Stream

WebSocket fan-out for live UX: server pushes message.created, message.updated (including inbound attachment progress), conversation.updated, participant.added, participant.removed envelopes, heartbeats (ping), and relays client typing and read events to other subscribers on the same routing key.

WS/api/v1/conversations/ws/conversations/{conversation_id}/stream

Conversation Stream (WebSocket)

Connect with JWT via ?token=, or send the token as the first text frame (raw token or JSON {"token": "..."}). Direct viewers require conversations:conversations:view and active participant membership; holders of agents:conversations:view may observe a participating agent’s public conversation without joining. Only active participants may send typing/read frames. Client may send JSON {type: typing}, {type: read, participant_id, timestamp}, or {type: pong}.

JWT via query or first frame. Active participant with conversations:conversations:view, or read-only derived access to a public participating-agent conversation

Path Parameters

NameTypeDescription
conversation_id*
string (UUID)Conversation ID
curl -i -N \
  -H "Connection: Upgrade" \
  -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" \
  -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
  "https://platform.ergondata.ai/api/v1/conversations/ws/conversations/{conversation_id}/stream?token={token}"

Response

101
{
  "type": "message.created",
  "message": {
    "id": "m3000000-0000-4000-8000-000000000003",
    "conversation_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "role": "user",
    "content": "Hello!",
    "created_at": "2026-04-21T12:15:00Z"
  }
}