Appearance
Streaming & WebSockets
Most of the API is plain request/response JSON. Three kinds of endpoint are not, because the work behind them takes seconds to minutes and users want to watch it happen: streamed text replies, Server-Sent Events, and three WebSocket channels that push live progress.
Streamed text replies
Endpoints that generate an answer with a language model return it as it is produced, in chunks, with Content-Type: text/event-stream and Transfer-Encoding: chunked.
Not SSE-framed
Despite the content type, the body of these endpoints is the raw answer text — there are no event:/data: lines. Read the body incrementally and concatenate the chunks. Some clients (browsers' EventSource) will refuse this; use fetch with a ReadableStream, curl -N, requests with stream=True, or similar.
| Endpoint | Body |
|---|---|
POST /secure-gpt/chat/ | Answer text |
POST /semantic-search/single-file-chat/, multi-file-chat/ | A JSON array of sources, immediately followed by the answer text |
POST /semantic-search/all-files-chat/ | A JSON array of source paths, then ||, then the answer text |
POST /semantic-search/advanced-search/, chat/, /spreadsheet-search/chat/ | Grouped sources, then the answer text |
POST /microservices/dashboard-chat/ | Answer text (as produced by the dashboard service) |
POST /workflow/run/stream/ | Answer text |
Splitting sources from the answer in the document chats: scan the leading [ … ] with bracket matching (strings may contain brackets), parse it as JSON, and take everything after it (dropping a leading ||) as the answer. Until the closing bracket has arrived, nothing is displayable yet.
python
import json, requests
r = requests.post(
"https://api.example.com/semantic-search/single-file-chat/",
headers={"Authorization": f"Token {TOKEN}"},
data={"query": "What are the key risks?", "document_name": "Q3-report.pdf",
"file_uid": "1b6c1c1e-…"},
stream=True,
)
buf = ""
for chunk in r.iter_content(chunk_size=None, decode_unicode=True):
buf += chunk
# display buf (minus the leading sources array) as it grows
print(buf)Server-Sent Events
Two endpoints emit real SSE (event: + data: frames separated by blank lines). Standard SSE clients work.
| Endpoint | Events |
|---|---|
POST /microservices/prompt-2-workflow/agent/ with stream: true | progress, skeleton, workflow, message, error, done |
GET /microservices/dedicated-agent/runs/{run_id}/events/ | The agent gateway's run events (tool calls, approvals, output) |
The copilot stream is a POST, so browsers must use fetch and parse frames themselves; curl -N shows them directly.
WebSockets
Live progress is pushed over WebSockets served by the same host as the API, under /ws/…. Use wss:// on HTTPS deployments.
Connecting
There is no Authorization header on a WebSocket handshake, so the access token is passed as a query parameter:
text
wss://api.example.com/ws/chat/<conversation_id>?token=<access_token>The server checks the token and that the user may see the resource, then accepts; otherwise it closes the socket immediately. Every channel answers {"type": "ping"} with {"type": "pong"} — send one every 30 seconds to keep proxies from dropping an idle connection, and reconnect with backoff if the socket closes (progress you missed is also available by polling the REST endpoints).
| Channel | Path | Who may connect | Pushes |
|---|---|---|---|
| Workflow run | /ws/workflow/{workflow_id}/nodes/ | Owner, collaborators | Node status changes, agent-node messages, definition updates |
| Chat AI turn | /ws/chat/{conversation_id} | The conversation's owner | Stage telemetry, streamed partial answer, the final answer |
| Dedicated agent | /ws/dedicated-agent/{agent_id}/ | The agent's owner | Same telemetry envelope, plus produced files and workflow specs |
Workflow run channel
On connect the server sends the current node list; afterwards it pushes a frame whenever anything changes. Frames:
json
{"type": "workflow_nodes", "data": [ { …WorkflowNode… }, … ]}The full node list (same objects as GET /microservices/workflow/{id}/nodes/), sent on connect, after every status change, and in reply to ping (as "type": "pong").
json
{"type": "agent_response", "node_id": "<node execution id>", "data": { … }}A conversational node (an agent tool) sent something to the user — a question it needs answered (reply with POST /microservices/workflow/agent-message/) or a progress message. data.status is "telemetry" for progress frames (see the envelope below) and the payload otherwise carries the node's message.
json
{"type": "workflow_definition_updated", "data": { …json_spec… }, "actor": {"id": 57, "username": "omar"}}Someone saved the workflow (or a restore/share update changed it). actor is absent for automated updates.
Chat AI channel
While a POST /microservices/agent/chat/ turn runs, the agent's progress is relayed here. Every frame shares one envelope:
json
{"type": "agent_response", "node_id": "chat", "data": { … }}Progress frames have data.status == "telemetry" and a data.event_type:
event_type | data.event | Meaning |
|---|---|---|
stage:thinking, stage:planning, stage:writing, … | — | A status line ("Thinking…") |
stage:tool_started | {tool, step_id, step_description} | A tool call began |
stage:tool_finished | {tool, step_id, success, duration_ms, error} | It finished |
stage:partial_answer | {text} | A chunk of the answer as it is written — chunks are sometimes cumulative snapshots rather than pure deltas; if a chunk starts with what you already have, replace instead of append |
stage:memory_recall | {hit_count, top_kinds, top_texts} | Memories injected at the start of the turn |
stage:done | — | The turn finished |
stage:memory_persisted, stage:memory_skipped | {text, kind, memory_id, confidence, superseded_id} | Post-turn bookkeeping: the agent stored (or decided not to store) a long-term memory |
stage:error | {message} | The turn failed |
The final answer arrives as a non-telemetry frame with data.message (and data.mgid / data.dashboard_id when the turn produced a chart or a Studio page). The HTTP response of the POST remains authoritative when it completes; the socket is what delivers the answer when the HTTP call timed out (504) or was queued.
json
{"type": "agent_response", "node_id": "chat",
"data": {"status": "telemetry", "event_type": "stage:tool_started",
"event": {"tool": "web_search", "step_id": "s1", "step_description": "Search for competitor news"}}}
{"type": "agent_response", "node_id": "chat",
"data": {"status": "telemetry", "event_type": "stage:partial_answer", "event": {"text": "Q3 revenue grew "}}}
{"type": "agent_response", "node_id": "chat",
"data": {"status": "completed", "message": "Q3 revenue grew **12.4%** year over year…", "turn_id": "7c1f…"}}Ownership of a conversation_id is claimed by the first authenticated user who posts to it; the socket refuses anyone else.
Dedicated agent channel
The dedicated agent console is driven entirely by this channel: after POST /microservices/dedicated-agent/chat/stream returns 202, every stage event, partial answer and the final answer arrive here in the same envelope as the Chat AI channel, with two additional frames:
event_type | data.event | Meaning |
|---|---|---|
stage:files | {urls: […]} | Media URLs of files the agent produced this turn |
stage:workflow | {json_spec, workflow_id, content_revision, summary, questions, suggestions, warnings, route, phase} | A workflow the agent built or modified (phase: "skeleton" first, then the final spec) — used when the workflow copilot runs on a dedicated agent |
Frames carry the chat session they belong to, so one socket serves every conversation with the agent.
Choosing polling instead
Every live channel has a polling equivalent, which is simpler for batch integrations:
| Instead of | Poll |
|---|---|
| Workflow run channel | GET /microservices/workflow/{id}/nodes/ every few seconds until every node is COMPLETED/FAILED/SKIPPED, or GET …/webhook/status/ with a webhook credential |
| Chat AI channel | Just wait for the POST to return (pass a generous timeout) |
| Dedicated agent channel | GET /microservices/dedicated-agent/messages/ for the persisted answer; GET …/runs/{run_id}/ for runs |