Agents API
Invoke Coherence AI agents programmatically with a single synchronous call
The Agents API lets you send a natural-language message to a Coherence agent and receive its final response in a single synchronous call. The agent runs its multi-step tool loop server-side — querying records, searching your workspace, drafting content, creating tasks — and returns a natural-language answer when it finishes.
Any tool the agent calls flows through your workspace's approval system, exactly as it does in the app. An API caller cannot use an agent to bypass approval gates: gated actions (sending email, external side effects, and so on) are queued for approval rather than executed directly.
Send a Message
POST https://api.getcoherence.io/v1/agents/messages
Requires an API key with the agents:write scope. See Authentication.
The endpoint runs in two modes over the same request: a synchronous JSON response by default, or an SSE stream when you opt in with an Accept: text/event-stream header or a ?stream=true query parameter. The request body and scope are identical in both modes.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
message | string | Yes | The instruction or question for the agent (1–10,000 characters) |
agentId | string (UUID) | No | Target a specific agent in your workspace. Omit to use the workspace's default agent. |
Example Request
curl -X POST "https://api.getcoherence.io/v1/agents/messages" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": "How many open deals do we have over $50k, and who owns them?"
}'Example Response
{
"response": "You have 4 open deals over $50k. Acme Corp ($120k) and Initech ($75k) are owned by Sarah Chen; Globex ($64k) and Hooli ($52k) are owned by Mike Johnson.",
"success": true,
"durationMs": 8421,
"toolCalls": 3
}Response Fields
| Field | Type | Description |
|---|---|---|
response | string | The agent's final natural-language answer |
success | boolean | Whether the agent run completed successfully |
durationMs | number | Server-side duration of the run in milliseconds |
toolCalls | number | Number of tool invocations the agent made |
error | string | Present only when the run failed |
Behavior Notes
- Synchronous — the request stays open while the agent works. Runs involving several tool calls can take tens of seconds; set your client timeout to at least 60 seconds.
- Bounded — each call is capped at a fixed number of tool-loop steps, so a single request completes within roughly the timeout window of typical HTTP and MCP clients. For long multi-part work, send follow-up messages rather than one giant instruction.
- Approval gates apply — if the agent attempts a gated action, the action is queued for approval in your workspace and the agent reports that in its response instead of performing it.
- Agent targeting — when
agentIdis provided, it must reference an agent in your workspace; otherwise the request fails with404.
Errors
Errors use the standard Coherence envelope (see Error Handling):
{
"error": {
"code": "not_found",
"message": "Agent not found",
"statusCode": 404
}
}| Status | When |
|---|---|
| 400 | message missing, empty, or over 10,000 characters; agentId not a valid UUID |
| 401 | Missing or invalid API key |
| 403 | API key lacks the agents:write scope |
| 404 | agentId does not match an agent in your workspace |
Streaming responses
The same endpoint can stream the run over Server-Sent Events (SSE) so you can render the agent's answer token-by-token and observe tool calls as they happen, instead of waiting for the whole run to finish.
Opt in either way — the request body and agents:write scope are unchanged:
- Send an
Accept: text/event-streamrequest header, or - Add a
?stream=truequery parameter to the URL.
The response comes back with Content-Type: text/event-stream as a sequence of
named events. Each event is an event: line followed by a data: line
containing a JSON object:
| Event | data shape | Meaning |
|---|---|---|
open | { "agentId": string | null } | The stream is established. agentId echoes the resolved target agent, or null for the workspace default. |
token | { "text": string } | A chunk of the agent's final answer. Concatenate text across all token events to reconstruct the full answer. |
tool_call | { "tool": string, "label": string } | The agent invoked a tool. label is a human-readable description such as "Searching records". No tool arguments or results are exposed. |
done | { response, success, durationMs, toolCalls, error? } | Terminal success. The data is the same object as the synchronous JSON body (see Response Fields). |
error | { "message": string } | Terminal failure. The stream ends after this event. |
Privacy parity with the synchronous response. Only the agent's final answer
(token) and human-readable tool labels (tool_call) are streamed. Raw model
reasoning and raw tool output are never sent over the stream, exactly as they
are never included in the synchronous response.
Lines beginning with : are keepalive comments (sent roughly every 15 seconds to
hold the connection open) — ignore them. If the client disconnects, the run is
aborted server-side.
Streaming with curl
Use -N so curl doesn't buffer the stream, and set the Accept header:
curl -N -X POST "https://api.getcoherence.io/v1/agents/messages" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{
"message": "How many open deals do we have over $50k, and who owns them?"
}'The response is a stream of events (keepalive comments elided):
event: open
data: {"agentId":null}
event: tool_call
data: {"tool":"search_records","label":"Searching records"}
event: token
data: {"text":"You have 4 open deals over $50k. "}
event: token
data: {"text":"Acme Corp ($120k) and Initech ($75k) are owned by Sarah Chen."}
event: done
data: {"response":"You have 4 open deals over $50k. Acme Corp ($120k) and Initech ($75k) are owned by Sarah Chen.","success":true,"durationMs":8421,"toolCalls":3}
Streaming with fetch (JavaScript / TypeScript)
You cannot use the browser EventSource API. EventSource only issues
GET requests and can't send a request body or an Authorization header, both
of which this endpoint requires. Use fetch with a ReadableStream reader (as
below) on the server or in the browser instead.
const res = await fetch('https://api.getcoherence.io/v1/agents/messages', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
Accept: 'text/event-stream',
},
body: JSON.stringify({
message: 'How many open deals do we have over $50k, and who owns them?',
}),
});
const reader = res.body!.getReader();
const decoder = new TextDecoder();
let buffer = '';
let answer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// SSE frames are separated by a blank line.
const frames = buffer.split('\n\n');
buffer = frames.pop() ?? '';
for (const frame of frames) {
let eventName = 'message';
const dataLines: string[] = [];
for (const line of frame.split('\n')) {
if (line.startsWith(':')) continue; // keepalive comment
if (line.startsWith('event:')) eventName = line.slice(6).trim();
else if (line.startsWith('data:')) dataLines.push(line.slice(5).trim());
}
if (dataLines.length === 0) continue;
const data = JSON.parse(dataLines.join('\n'));
if (eventName === 'token') {
answer += data.text; // concatenate token chunks
} else if (eventName === 'tool_call') {
console.log(`Agent is: ${data.label}`);
} else if (eventName === 'done') {
console.log('Final answer:', data.response);
} else if (eventName === 'error') {
throw new Error(data.message);
}
}
}The full answer is available two ways: accumulate token events (answer above),
or read response from the final done event — they resolve to the same text.
Related: API Overview | Authentication | Records API