Chat API
Chat with an agent over the REST API, as one synchronous request or a server-sent event stream, and list the agents you can call.
The reference has a page for each of these endpoints, with every field and answer and samples in cURL, JavaScript, Python and Go: send, stream, cancel, approve, and the agents' list and get. /api/v2's chat starts at Send a chat message.
POST /api/v1/chat
Send a message to an agent and get a synchronous response. The request waits until the agent completes its full ReAct loop and returns the final answer. If the agent is an Orchestrator agent, its workflow runs and its final response is returned.
Request
POST /api/v1/chat
Authorization: Bearer sk-syn-...
Content-Type: application/json{
"message": "What is the weather in London right now?",
"agent": "Research Agent",
"session_id": "user-session-123",
"images": []
}Request body fields:
| Field | Type | Required | Description |
|---|---|---|---|
message | string | Yes | The user's message. @[path] vault mentions are read in, as in Chat |
agent | string | No | Agent ID, or its exact name in any case. An unknown agent is a 404; omitted, the workspace's first agent answers — see Agent resolution |
session_id | string | No | Session identifier for conversation continuity. Omitted, a new one (api_…) is generated, and the conversation starts fresh |
images | array | No | Base64 data-URI images for models that can see them — PNG, JPEG, GIF or WebP; at most 5, each up to 5 MB, 20 MB in total. e.g. "data:image/png;base64,...". An image that breaks these rules is a 422, and so are images for an agent that can't read them: This agent's model (…) cannot read images. Send the message without them, or choose a model that can for this agent. — refused before anything is queued |
Response
{
"response": "The current weather in London is 12°C and cloudy...",
"status": "completed",
"agent_id": "agent_1726412345678",
"agent_name": "Research Agent",
"session_id": "user-session-123",
"refused_calls": []
}Response fields:
| Field | Type | Description |
|---|---|---|
response | string | The agent's final response text — or, when the turn stopped for a person, the question it stopped on |
status | string | completed, awaiting_approval or paused — see below — or cancelled, when the turn was stopped |
agent_id | string | ID of the agent that responded |
agent_name | string | Display name of the agent |
session_id | string | Session ID — send it with the next request to continue the conversation |
refused_calls | array | The tool calls this turn did not run, each {tool_name, category, reason} — refused by Tool Policy or by a person. Always present; empty when every call ran. The answer is the model's own words and can say something was done when its only call was refused, so check this list |
citations | array | The knowledge passages the answer cited, each with its label, file, pages, section, snippet, when it was indexed and its quote check — see Search. Always present; empty when it cited none |
unresolved_citations | array | Labels the answer cited that no search in this turn returned |
If the agent fails, the request returns 500, and the details are in the server log and in Runs › Logs. The provider's error is never returned as the agent's response. The body says which kind of failure it was:
-
The model failed.
detailstarts The agent encountered an error processing your request. and adds a sentence for the kind, withcodeandretryablebeside it:codeMeans What to do quota_exhaustedThe provider refused it: the credit or quota is exhausted Top up or raise the limit with the provider; retrying will not help provider_refusedThe provider refused it — a key it does not accept, a model it does not serve Fix the agent's model or the workspace's model settings rate_limitedThe provider is limiting requests Retry shortly provider_unavailableThe provider could not be reached, or failed Retry shortly model_no_answerThe model answered with nothing usable — empty, blocked, or only reasoning Try another model, or rephrase model_errorAny other model failure (no sentence added) Read the turn in the app's logs retryableistrueforrate_limitedandprovider_unavailableonly.{ "detail": "The agent encountered an error processing your request. The model provider is limiting requests right now. Try again shortly.", "code": "rate_limited", "retryable": true } -
An Orchestrator agent's workflow failed.
detailnames the run to read back: The agent's workflow failed before answering. Read run<run_id>at /api/v1/orchestrations/runs/<run_id>. See reading a run. -
Anything else.
detailis The agent encountered an error processing your request., with nocode.
A session writes one reply at a time: a message to a session whose turn is still running returns 409 (A reply is still being written in this conversation).
When the turn stops for a person
awaiting_approval — the agent reached a tool call its Tool Policy wants confirmed. The call has not run. pending_call describes it:
{
"response": "The agent wants to run **scrape_url** — it reaches systems outside this workspace. Allow it?",
"status": "awaiting_approval",
"pending_call": {
"tool_name": "scrape_url",
"tool_args": { "url": "https://example.com" },
"category": "open_world",
"approval_id": "3f9a1c2b7d4e5f60"
},
"agent_id": "agent_1726412345678",
"agent_name": "Research Agent",
"session_id": "user-session-123"
}paused — the agent is an Orchestrator agent and its workflow paused on a Human step. run_id names the run, and response holds the step's question:
{
"response": "Review the draft and approve or reject it.",
"status": "paused",
"run_id": "run_orch_k3j9x2a_1734567890123",
"agent_id": "agent_1726412345678",
"agent_name": "Report Pipeline",
"session_id": "user-session-123"
}Answer a call waiting for approval with POST /api/v1/chat/{session_id}/approve, quoting its approval_id. A paused workflow is answered through its resume endpoint with the run_id, or in the app.
Sending the conversation a new message instead of answering refuses the waiting call: it is not run, the agent is told, and the call cannot be allowed afterwards — an approve sent later is a 409.
cURL example
curl -X POST http://localhost:3000/api/v1/chat \
-H "Authorization: Bearer sk-syn-..." \
-H "Content-Type: application/json" \
-d '{
"message": "Summarise the latest news about AI agents",
"agent": "Research Agent",
"session_id": "my-session"
}'Python example
import requests
response = requests.post(
"http://localhost:3000/api/v1/chat",
headers={"Authorization": "Bearer sk-syn-..."},
json={
"message": "What is 15% of $2,340?",
"agent": "Finance Assistant",
"session_id": "calc-session",
},
)
data = response.json()
print(data["response"])POST /api/v1/chat/stream
Send a message to an agent and receive a stream of Server-Sent Events (SSE). This lets you display progress and tool calls in real time.
Request
Same as /chat — same fields, same auth.
SSE Event Stream
The response is a stream of data: events, each a JSON object, plus : ping comment lines after 10 seconds without an event — skip any line that starts with :.
Event types:
| Type | Description | Fields |
|---|---|---|
session | First event — session metadata | session_id, agent_id, agent_name |
status | Status update | message |
thinking | A progress message, such as "Analyzing your request..." — not the model's reasoning | message |
tool_execution | Agent is calling a tool | tool_name, args |
tool_result | Tool call completed — or was not run: then refused is true, with the reason and the policy category | tool_name, preview; refused, reason, category when not run |
llm_thought | The agent's reasoning text for a turn | thought, turn |
answer_delta | The answer as the model writes it; response is still the answer of record | text, turn |
answer_reset | What streamed since the last reset was not the answer (the model went on to call a tool): drop it | reason, turn |
knowledge_search | The agent searched its knowledge bases | query_id, kb_ids, query, hit_count, labels, degraded, cost_usd, latency_ms |
knowledge_read | The agent read around a passage | label, path, window |
knowledge_citations | Just before response: the passages the answer cited — only this turn's own, not a Team member's | citations, unresolved_citations |
response | Final response | content, intent, session_id |
awaiting_approval | A tool call is waiting for approval and has not run — see When the turn stops for a person | prompt, pending_call, session_id |
paused | An Orchestrator agent's workflow paused on a Human step | prompt, run_id, session_id |
agent_step_result | An Orchestrator agent's workflow finished one of its agent steps — not the turn | orch_step_id, step_name, content |
cancelled | The turn was stopped | message |
error | The agent failed. The message says which kind of failure it was, as for /chat; a model failure adds code and retryable, a failed workflow adds run_id. The provider's own words are never sent | message; code, retryable or run_id |
done | The last event: after response, awaiting_approval, paused, cancelled or error | — |
A stream that ends with error has no response. Other events the engine produces — task list updates, for example — are not forwarded.
cURL example
curl -X POST http://localhost:3000/api/v1/chat/stream \
-H "Authorization: Bearer sk-syn-..." \
-H "Content-Type: application/json" \
-N \
-d '{"message": "Research quantum computing trends", "agent": "Research Agent", "session_id": "stream-test"}'Example stream output:
data: {"type": "session", "session_id": "stream-test", "agent_id": "agent_1726412345678", "agent_name": "Research Agent"}
data: {"type": "thinking", "message": "Analyzing your request..."}
data: {"type": "tool_execution", "tool_name": "scrape_url", "args": {"url": "https://arxiv.org/..."}}
data: {"type": "tool_result", "tool_name": "scrape_url", "preview": "Quantum computing in 2024..."}
data: {"type": "response", "content": "Here are the key trends in quantum computing...", "intent": "chat", "session_id": "stream-test"}
data: {"type": "done"}JavaScript example
const response = await fetch("http://localhost:3000/api/v1/chat/stream", {
method: "POST",
headers: {
"Authorization": "Bearer sk-syn-...",
"Content-Type": "application/json",
},
body: JSON.stringify({
message: "Write a Python script to parse JSON",
agent: "Code Assistant",
session_id: "js-session",
}),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop(); // keep a partial line for the next chunk
for (const line of lines) {
if (!line.startsWith("data: ")) continue; // skips ": ping" and blank lines
const event = JSON.parse(line.slice(6));
if (event.type === "response") {
console.log("Final:", event.content);
} else if (event.type === "tool_execution") {
console.log("Using tool:", event.tool_name);
}
}
}The turn is not the connection
A turn belongs to the server, not to the request that started it. A caller that stops reading — a timeout, a crash, a dropped network — leaves the turn running to the end, and its answer is saved to the session. To stop a turn, cancel it.
POST /api/v1/chat/{session_id}/cancel
Stop the turn running in a session.
curl -X POST http://localhost:3000/api/v1/chat/stream-test/cancel \
-H "Authorization: Bearer sk-syn-..."{"status": "cancellation_requested", "session_id": "stream-test", "stopped": true}stopped is false when nothing was running — the turn had already finished. That is not an error. A turn inside a long model call stops when that call returns; its stream ends with cancelled, then done.
POST /api/v1/chat/{session_id}/approve
Answer the tool call a conversation stopped on (status: "awaiting_approval"), and carry the turn on. The answer is a stream, in the same events as /chat/stream.
curl -X POST http://localhost:3000/api/v1/chat/user-session-123/approve \
-H "Authorization: Bearer sk-syn-..." \
-H "Content-Type: application/json" \
-N \
-d '{"approve": true, "approval_id": "3f9a1c2b7d4e5f60"}'| Field | Type | Required | Description |
|---|---|---|---|
approve | boolean | Yes | true runs the call; false refuses it |
reason | string | No | Why it was refused — the agent is told |
approval_id | string | No | The approval_id from pending_call. When given and it is not the call the conversation is waiting on, the answer is a 409 rather than an answer to a newer question |
agent | string | No | The conversation's agent. Omitted, the agent that is waiting; name it (or the approval_id) when two agents in one conversation both wait |
Approved, the call runs and the stream carries on to the answer. Refused, the stream shows the call as tool_execution, then a tool_result with refused, reason and category, and the agent carries on without it.
A conversation that is not waiting on an approval answers 409 (This conversation is not waiting on an approval.), and so does one whose turn is still running.
GET /api/v1/agents
List the workspace's agents. Synapse AI's own agents are not listed, and the chat endpoints never run them.
curl http://localhost:3000/api/v1/agents \
-H "Authorization: Bearer sk-syn-..."[
{
"id": "agent_1726412345678",
"name": "Research Agent",
"type": "conversational",
"model": "claude-opus-5",
"capabilities": []
}
]model is empty when the agent uses the default model. capabilities is always an empty list.
GET /api/v1/agents/{agent_id}
Get details for a specific agent. Returns 404 if there is no agent with that ID, or if it is one of Synapse AI's own.
curl http://localhost:3000/api/v1/agents/agent_1726412345678 \
-H "Authorization: Bearer sk-syn-..."{
"id": "agent_1726412345678",
"name": "Research Agent",
"type": "conversational",
"model": "claude-opus-5",
"capabilities": [],
"description": "Deep research using web browsing"
}REST API Reference
SynapseOrch exposes a REST API for integrating agents and orchestrations into external applications. All endpoints require an API key for authentication.
Workflows API
List orchestrations, run them synchronously or as a stream, read a run back after its stream is gone, resume runs that paused at a human gate, and cancel them.