Agent Chat API
Send Agent messages and receive text, sources, tools, and media results
The Chat API allows you to send messages to agents, stream responses, manage runs, and handle conversations. Chat endpoints live under /api/v1/agents; conversation-management endpoints live under /api/v1/conversations and /api/v1/agents/conversations.
Authentication
POST /api/v1/agents/{agent_id}/chat and /chat/stream support either a JWT bearer token or an API key. Conversation management and message-version routes require the authenticated JWT user who owns the conversation.
Required scopes:
agent:read- View agentsagent:chat- Chat with agentsconversation:read- List and view conversationsconversation:delete- Delete conversations
The Agent must be published. The API Key needs the matching permission, and if bound to an Agent, it can only access IDs in the allowlist. The Agent's attachment limits, tool iteration caps, and model quotas still apply.
Endpoint Overview
| Method | Path | Purpose |
|---|---|---|
| GET | /api/v1/agents/{agent_id}/public | Get public Agent basic info, authentication optional |
| POST | /api/v1/agents/{agent_id}/chat | Non-streaming chat |
| POST | /api/v1/agents/{agent_id}/chat/stream | SSE streaming chat |
| POST | /api/v1/agents/{agent_id}/chat/runs | Queue a durable run, returns 202 Accepted immediately |
| GET | /api/v1/agents/{agent_id}/chat/runs/{run_id}/stream?after_sequence=N | Subscribe to a run's event stream (resumable) |
| POST | /api/v1/agents/{agent_id}/chat/runs/{run_id}/stop | Cooperatively stop a run |
| POST | /api/v1/agents/{agent_id}/messages/{message_id}/regenerate | Regenerate |
| POST | /api/v1/agents/{agent_id}/messages/{message_id}/edit/stream | Edit a user message and create a branch |
| GET | /api/v1/agents/{agent_id}/messages/{message_id}/versions | Query message versions |
| POST | /api/v1/agents/{agent_id}/messages/{message_id}/switch-version | Switch version |
| GET | /api/v1/agents/{agent_id}/chat/runs/{run_id} | Run status (owner-scoped) |
| GET | /api/v1/agents/{agent_id}/chat/runs/{run_id}/events?after_sequence=N | Replay buffered events after N |
| POST | /api/v1/agents/{agent_id}/chat/runs/{run_id}/inputs | Queue steering/follow-up (delivery: steer / follow_up / auto) |
| POST | /api/v1/agents/{agent_id}/chat/runs/{run_id}/answers | Submit structured answers for a waiting ask_user call |
| GET | /api/v1/conversations | List conversations |
| GET | /api/v1/conversations/{conversation_id} | Get a conversation and its messages |
| PATCH | /api/v1/agents/conversations/{conversation_id} | Update the conversation title |
| DELETE | /api/v1/conversations/{conversation_id} | Delete a conversation |
Send Message
Send a message to an agent and receive a response.
Endpoint
POST /api/v1/agents/{agent_id}/chatPath Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
agent_id | string | Yes | Agent UUID |
Request Body
{
"message": "What are your business hours?",
"conversation_id": "conv-123",
"file_urls": [
{
"asset_id": "asset-456",
"url": "https://your-domain.com/api/v1/upload/files/general/2026/09/7f3a1c9d2b10_a1b2c3d4.pdf",
"filename": "7f3a1c9d2b10_a1b2c3d4.pdf",
"size": 1048576,
"mime_type": "application/pdf"
}
],
"variables": {
"customer_tier": "premium"
}
}Submit url, filename, size, and mime_type exactly as returned by the upload response; do not construct /upload/files/{asset_id} from the asset ID. A generated image or video asset_ref is valid only within its conversation or workflow-run scope; protected media URLs require the active JWT or API key.
Request Fields
| Field | Type | Required | Description |
|---|---|---|---|
message | string | Yes | User message (max 32000 chars) |
images | array | No | Vision images (asset_id or asset_ref, type, url) |
files | array | No | Parsed files for upload (deprecated, use file_urls) |
file_urls | array | No | Asset metadata from the upload response (asset_id, filename, url, size, mime_type) |
conversation_id | string | No | Conversation UUID (creates new if not provided) |
variables | object | No | Variable values for the chat input form |
history_override | array | No | Override conversation history (used for version switching/regeneration) |
Request Example
curl -X POST "https://your-domain.com/api/v1/agents/agent-123/chat" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"message": "What are your business hours?",
"conversation_id": "conv-123"
}'Non-Streaming Response
Success (200 OK):
{
"code": 0,
"data": {
"conversation_id": "conv-123",
"message": {
"id": "msg-456",
"conversation_id": "conv-123",
"role": "assistant",
"content": "Our business hours are Monday-Friday, 9 AM to 5 PM EST.",
"tool_calls": [],
"tool_name": null,
"model_used": "gpt-4",
"token_usage": {
"prompt": 150,
"completion": 25,
"total": 175
},
"duration_ms": 2300,
"rag_context": [
{
"document_id": "doc-789",
"document_name": "Business Hours Policy",
"chunk_id": "chunk-012",
"content": "Business hours: Monday-Friday, 9 AM to 5 PM EST",
"score": 0.95,
"citation_id": "rag_0123456789abcdef"
}
],
"created_at": "2026-02-11T16:00:00Z",
"version_number": 1,
"version_count": 1
},
"usage": {
"prompt": 150,
"completion": 25,
"total": 175
}
},
"msg": "success"
}rag_context is null when retrieval did not run (for example, RAG is off or the Agent has no knowledge-base associations). It is [] when retrieval ran but returned no contexts. Retrieved contexts may include a stable citation_id; cite the exact ID with [[cite:SOURCE_ID]].
Streaming Response
Streaming is a separate endpoint; there is no stream flag on POST /chat.
Endpoint: POST /api/v1/agents/{agent_id}/chat/stream
The request body is the same ChatRequest payload. The response is sent as Server-Sent Events (SSE).
Content-Type: text/event-stream
Event format:
event: message_start
data: {"conversation_id": "conv-123", "message_id": "msg-456"}
event: content_delta
data: {"delta": "Our"}
event: content_delta
data: {"delta": " business"}
event: content_delta
data: {"delta": " hours"}
event: rag_context
data: {"contexts": [{"document_name": "FAQ", "content": "...", "score": 0.95}]}
event: message_end
data: {"usage": {"prompt_tokens": 150, "completion_tokens": 25, "total_tokens": 175}, "timing": {"first_token_ms": 320, "duration_ms": 2300, "tokens_per_second": 10.9}}See SSE Streaming for details.
media_result is a UI event for successfully generated media and is not replayed to the model as text:
Clients should preserve received SSE event order. Retrieval, compression, reasoning, tool, media, and text events may interleave; rag_start and rag_context are emitted only when retrieval actually runs.
event: media_result
data: {"kind":"media.image","success":true,"images":[{"image":{"url":"/api/v1/upload/files/generated-images/2026/09/9a8b7c6d5e4f_55667788.png","asset_ref":"a1b2","format":"png"}}]}Fetch protected generated-image and generated-video URLs with the active JWT or API key. Clients should surface unauthenticated, unauthorized, unavailable, and too-large preview states instead of falling back to an unauthenticated raw URL.
Durable Runs and Control
Chat execution is backed by a durable AgentRun so a browser disconnect never stops the model loop. /chat and /chat/stream wait for the round's result; /chat/runs instead persists the request and enqueues it immediately for a worker, so a dropped connection or page refresh never loses the turn.
Run-scoped events carry an extra envelope on top of the regular SSE events — run_id, sequence, timestamp, round_id, message_id, and type — so clients can resume with after_sequence and render nothing twice.
Additional run events:
| Event | Description |
|---|---|
run_start | Run identity and initial status |
run_status | Lifecycle transitions (queued → running → …); the run enters waiting when the model calls ask_user, carrying pending_tool_call_id, pending_tool_name, and pending_tool_input |
input_accepted | A queued steering/follow-up input was committed |
run_end | Exactly one terminal event per run, with status completed, stopped, failed, or interrupted |
| Method | Path | Purpose |
|---|---|---|
| POST | /api/v1/agents/{agent_id}/chat/runs | Persist and enqueue a run (202 Accepted) |
| GET | /api/v1/agents/{agent_id}/chat/runs/{run_id}/stream?after_sequence=N | SSE-subscribe to the run's event stream (resumable) |
| GET | /api/v1/agents/{agent_id}/chat/runs/{run_id} | Run status (owner-scoped) |
| GET | /api/v1/agents/{agent_id}/chat/runs/{run_id}/events?after_sequence=N | Replay buffered events (non-streaming; handy after packet loss) |
| POST | /api/v1/agents/{agent_id}/chat/runs/{run_id}/inputs | Queue steering/follow-up/stop (delivery: steer / follow_up / auto) |
| POST | /api/v1/agents/{agent_id}/chat/runs/{run_id}/answers | Submit structured answers for a waiting ask_user call |
| POST | /api/v1/agents/{agent_id}/chat/runs/{run_id}/stop | Cooperatively stop a run |
Start a Run
POST /api/v1/agents/{agent_id}/chat/runsThe request body is identical to POST /api/v1/agents/{agent_id}/chat (the same ChatRequest); the difference is that it returns 202 Accepted immediately instead of waiting for the assistant reply. The response data is a RunStartOut:
{
"code": 0,
"data": {
"run_id": "8f3b1c9d-2b10-4a1b-9c3d-7f3a1c9d2b10",
"conversation_id": "conv-123",
"user_message_id": "msg-456",
"status": "queued",
"stream_url": "/agents/agent-123/chat/runs/8f3b1c9d-2b10-4a1b-9c3d-7f3a1c9d2b10/stream"
},
"msg": "success"
}| Field | Type | Description |
|---|---|---|
run_id | string | Durable run UUID |
conversation_id | string | Conversation UUID (created when the request omitted conversation_id) |
user_message_id | string | The persisted user message UUID |
status | string | Run status at enqueue time (see the status values below) |
stream_url | string | Relative path of the run's SSE stream, without the /api/v1 prefix |
stream_url is relative and only contains /agents/... (/embed/agents/... for embed). You must prepend /api/v1 yourself when building the subscription URL.
Stream a Run
GET /api/v1/agents/{agent_id}/chat/runs/{run_id}/stream?after_sequence=NReturns text/event-stream: it first replays every buffered event whose sequence is greater than after_sequence (default 0), then follows live events until the run's terminal event. Accepts a JWT bearer token or an API key.
Reconnection semantics: track the highest sequence you have processed and, after a disconnect, request again with that value as after_sequence to continue with no duplicates and no gaps. Negative after_sequence values are clamped to 0. For a non-streaming catch-up, read the same buffered events from GET .../events?after_sequence=N.
Run status values (RunOut.status / RunStartOut.status): queued, running, stopping, completing, waiting, completed, stopped, failed, interrupted. Of these, completed, stopped, failed, and interrupted are terminal.
Stop a Run
POST /api/v1/agents/{agent_id}/chat/runs/{run_id}/stopNo request body. Cooperatively stops a queued, running, or waiting run and returns the updated RunOut; the event stream then receives a terminal run_end with status: "stopped". Stopping a waiting run (blocked on an ask_user answer) works the same way.
RunOut fields: id, agent_id, conversation_id, mode, status, source_message_id, canonical_message_id, active_round_id, error_code, error_message, started_at, finished_at, plus the pending ask_user interaction (pending_tool_call_id, pending_tool_name, pending_tool_input).
Access boundaries: every run route is owner-scoped (agent-scoped for an API key). An unknown run — or one that does not belong to the {agent_id} in the path — returns 404 (code 4000, run_not_found); a run whose conversation belongs to another user returns 403 (code 1004, access_denied); superusers bypass the ownership check.
Submit answers for a waiting ask_user call:
POST /api/v1/agents/{agent_id}/chat/runs/{run_id}/answersRequest body:
{
"tool_call_id": "call-123",
"answers": {
"deployment_target": "cloud"
},
"skipped": false
}| Field | Type | Required | Description |
|---|---|---|---|
tool_call_id | string | Yes | Pending tool-call ID from the run_status waiting event |
answers | object | No | ID-keyed answer map; each value is the chosen option or a custom string |
skipped | boolean | No | Explicitly skip every question (sends { "answers": {}, "skipped": true }) |
The run must be in waiting status and the tool_call_id must match the pending interaction. On success the run resumes from its paused state.
While a run is active, the composer can queue steering (mid-work) or follow-up (final boundary) instead of being disabled; stop sends a server command and waits for the terminal run_end event.
Regenerate Response
Regenerate an assistant message. This endpoint lives under the agents router and is addressed by agent_id + message_id (no conversation_id in the path).
Endpoint
POST /api/v1/agents/{agent_id}/messages/{message_id}/regeneratePath Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
agent_id | string | Yes | Agent UUID |
message_id | string | Yes | Message UUID to regenerate |
Request Body
{
"variables": {}
}Request Example
curl -X POST "https://your-domain.com/api/v1/agents/agent-123/messages/msg-002/regenerate" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"variables": {}
}'Response
Success (200 OK):
{
"code": 0,
"data": {
"conversation_id": "conv-123",
"message": {
"id": "msg-003",
"conversation_id": "conv-123",
"role": "assistant",
"content": "Our business hours are Monday through Friday, from 9:00 AM to 5:00 PM Eastern Standard Time.",
"token_usage": {
"total": 180
},
"created_at": "2026-02-11T16:15:00Z",
"version_number": 2,
"version_count": 2
},
"usage": {
"total": 180
}
},
"msg": "success"
}Edit User Message and Regenerate (SSE)
Edit a user message, create a new version, and stream the regenerated reply:
POST /api/v1/agents/{agent_id}/messages/{message_id}/edit/streamRequest body:
{"content": "What are your holiday hours?"}Only user messages can be edited. The response is an authenticated SSE stream (text/event-stream) using the chat stream event format; the final message_end event includes edited_version_number and edited_version_count.
Message Versions
List Versions
List the versions in a message's version group. This route requires the authenticated owner of the conversation.
GET /api/v1/agents/{agent_id}/messages/{message_id}/versionsThe 200 OK response is an array of message-version objects containing id, version_number, is_active, content, and created_at.
Switch Version
Activate a different version in the same message group. Versions after the selected message are deactivated because they were based on the previous branch.
POST /api/v1/agents/{agent_id}/messages/{message_id}/switch-versionRequest body:
{"version_id": "msg-version-003"}The 200 OK response returns the selected MessageOut, including version information. The authenticated user must own the conversation.
Conversation Management
List Conversations
GET /api/v1/conversations| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
page | integer | No | 1 | Page number |
page_size | integer | No | 20 | Items per page (max: 100) |
team_id | string | No | - | Filter by team |
agent_id | string | No | - | Filter by agent ID |
user_id | string | No | - | Filter by user (admin/dashboard access only) |
search | string | No | - | Match the conversation title (does not search message content) |
untitled_only | boolean | No | false | Show only untitled conversations (takes precedence; search is ignored when both are supplied) |
Success (200 OK):
{
"code": 0,
"data": {
"items": [
{
"id": "conv-123",
"agent_id": "agent-456",
"agent_name": "Customer Support Agent",
"agent_icon": "🤖",
"title": "Business Hours Inquiry",
"message_count": 5,
"created_at": "2026-02-11T16:00:00Z",
"updated_at": "2026-02-11T16:05:00Z",
"user_id": "user-123",
"user_name": "alice"
}
],
"total": 42,
"page": 1,
"page_size": 20
},
"msg": "success"
}Get Conversation
GET /api/v1/conversations/{conversation_id}Success (200 OK):
{
"code": 0,
"data": {
"id": "conv-123",
"agent_id": "agent-456",
"agent_name": "Customer Support Agent",
"agent_icon": "🤖",
"title": "Business Hours Inquiry",
"variables": {},
"message_count": 5,
"token_usage": 875,
"created_at": "2026-02-11T16:00:00Z",
"updated_at": "2026-02-11T16:05:00Z",
"messages": [
{
"id": "msg-001",
"conversation_id": "conv-123",
"role": "user",
"content": "What are your business hours?",
"created_at": "2026-02-11T16:00:00Z",
"version_number": 1,
"version_count": 1
},
{
"id": "msg-002",
"conversation_id": "conv-123",
"role": "assistant",
"content": "Our business hours are Monday-Friday, 9 AM to 5 PM EST.",
"tool_calls": [],
"rag_context": [
{
"document_id": "doc-789",
"document_name": "Business Hours Policy",
"score": 0.95
}
],
"created_at": "2026-02-11T16:00:02Z",
"version_number": 1,
"version_count": 1
}
]
},
"msg": "success"
}Paginated GET /conversations/{id}/messages is not implemented / Roadmap. Messages are included inline in GET /api/v1/conversations/{conversation_id}.
Update Conversation
Update conversation details (e.g. rename). This endpoint lives under the agents router.
PATCH /api/v1/agents/conversations/{conversation_id}Request body:
{
"title": "Updated Conversation Title"
}Success (200 OK):
{
"code": 0,
"data": {
"id": "conv-123",
"title": "Updated Conversation Title",
"updated_at": "2026-02-11T16:10:00Z"
},
"msg": "Conversation updated successfully"
}Delete Conversation
Permanently delete a conversation (messages cascade). DELETE /api/v1/conversations is mounted on the platform conversation router and requires conversation:delete; superusers/admins can delete any conversation in an accessible team, while an ordinary member can only delete their own (someone else's conversation returns 404).
DELETE /api/v1/conversations/{conversation_id}Success (200 OK):
{
"code": 0,
"data": {
"id": "conv-123"
},
"msg": "Conversation deleted successfully"
}Batch Delete Conversations
DELETE /api/v1/conversations?ids=conv-123&ids=conv-456ids is a required repeated query parameter (list[UUID]) that deletes several conversations at once; the permission rules match single deletion, and any inaccessible target rejects the whole batch. When none of the targets exist the call returns 404 (6210).
curl -X DELETE "https://your-domain.com/api/v1/conversations?ids=conv-123&ids=conv-456" \
-H "Authorization: Bearer YOUR_TOKEN"Delete a Single Message
The platform conversation router has no per-message delete endpoint; to delete one message use the user-scoped endpoint under the agents router:
DELETE /api/v1/agents/conversations/{conversation_id}/messages/{message_id}It requires conversation:delete, and the message must belong to the caller's conversation (see Agent Conversations in the Agent API).
Route capability differences: the platform /api/v1/conversations router only exposes GET list, GET detail, DELETE single, and DELETE batch. Renaming (PATCH) and deleting a single message exist only under the agents router (/api/v1/agents/conversations/...). The platform list's search also matches the conversation title only and never message content.
For user-scoped conversation endpoints (e.g.
GET /api/v1/agents/conversations/my), see Conversations API.
Error Codes
| Code | Message | Description |
|---|---|---|
6200 | Agent not found | Agent does not exist |
6210 | Conversation not found | Conversation does not exist |
6211 | Message not found | Message does not exist |
4000 | Run not found | Durable run does not exist or does not belong to the agent in the path |
1004 | Access denied | The run's conversation belongs to another user |
3000 | Permission denied | Insufficient permissions |
1001 | Validation failed | Invalid request data |
No per-endpoint rate limits are implemented. There is no rate-limit middleware on these endpoints.
For full error handling and retry strategies, see API Errors and Retries.
Best Practices
Message Handling
✅ Do:
- Keep messages concise and clear
- Provide context when needed
- Use
conversation_idto maintain context - Handle streaming for better UX
- Implement retry logic for failures
❌ Don't:
- Send extremely long messages
- Create new conversation for each message
- Ignore error responses
- Skip error handling
- Spam the API
Conversation Management
✅ Do:
- Use descriptive conversation titles
- Clean up old conversations
- Monitor conversation count
❌ Don't:
- Create unnecessary conversations
- Keep all conversations forever
- Forget to delete test conversations
Code Examples
Python
import requests
def chat_with_agent(agent_id, message, conversation_id=None):
"""Send message to agent."""
url = f"https://your-domain.com/api/v1/agents/{agent_id}/chat"
headers = {
"Authorization": "Bearer YOUR_TOKEN",
"Content-Type": "application/json"
}
data = {
"message": message,
"conversation_id": conversation_id
}
response = requests.post(url, headers=headers, json=data)
result = response.json()
if result['code'] == 0:
return result['data']
else:
raise Exception(f"Error: {result['msg']}")
# Usage
response = chat_with_agent(
agent_id="agent-123",
message="What are your business hours?",
conversation_id="conv-123"
)
print(f"Response: {response['message']['content']}")
print(f"Conversation ID: {response['conversation_id']}")JavaScript
async function chatWithAgent(agentId, message, conversationId = null) {
const url = `https://your-domain.com/api/v1/agents/${agentId}/chat`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_TOKEN',
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: message,
conversation_id: conversationId,
}),
});
const result = await response.json();
if (result.code === 0) {
return result.data;
} else {
throw new Error(result.msg);
}
}
// Usage
const response = await chatWithAgent(
'agent-123',
'What are your business hours?',
'conv-123'
);
console.log('Response:', response.message.content);
console.log('Conversation ID:', response.conversation_id);Related Documentation
- SSE Streaming - Streaming responses
- API Errors and Retries - Error details
- Conversations API - Conversation management
How is this guide?