Server Routes
Every request AgentServerClient makes, so you can build a compatible backend, debug a proxy, or call the API yourself. Paths are relative to baseUrl (for example /api/agents). The same shape is used by a Cognipeer Console portal (/api/portal/{key}), which also serves the embed session routes below.
The routes are served by @cognipeer/agent-server; the server-side reference is Endpoints.
Requests and errors
Every request carries the headers built from authorization, headers and getHeaders (the last one wins), and Content-Type: application/json where there is a body. Requests without a body (cancel, export, file content, the event stream) send no Content-Type.
A failed request rejects with an Error whose message is the server's and which carries:
| Field | From |
|---|---|
status | The HTTP status |
code | error.code, else error.type, else a top-level code of the answer |
details | error.details when it is an object (activeRunId, pendingActionIds, spentUsd / maxCostUsd, ...) |
The message is read from error.message, else error (a string), else message. Codes that have a localized string are listed in Error codes.
{ "error": { "code": "conversation_busy", "message": "A turn is already running", "details": { "activeRunId": "run_x1" } } }Agents
| Request | Answer |
|---|---|
GET /agents | { "agents": AgentInfo[] }. Chat reads capabilities.attachments, capabilities.approvalArgsEdit, capabilities.budget and metadata (including starters, welcome, experience) from here |
GET /agents/:agentId | An AgentInfo |
Conversations
| Request | Body / query | Answer |
|---|---|---|
GET /conversations | ?agentId&userId&limit&offset | { conversations, total, limit, offset, hasMore }. Items may carry runtime, budget, spentUsd, pinned / metadata.pinned |
POST /conversations | { agentId, title?, userId?, metadata? } | { conversation } |
GET /conversations/:id | ?before=<messageId>&limit=<n> | { conversation, messages, pendingActions?, messagesPage? }. messages is the newest page (100 by default); messagesPage is { limit, order, hasMoreBefore, hasMoreAfter }; before returns the page just older than that message. conversation carries runtime, budget, spentUsd, continuable |
PATCH /conversations/:id | { title? }, { pinned }, { budget } (null removes it), { metadata? } | The updated conversation (or { conversation }). A budget change answers with the runtime it is in now and continuable |
DELETE /conversations/:id | No body is needed (a 204 is accepted) | |
POST /conversations/bulk-delete | { ids } (at most 100) | { results: [{ id, status: "deleted" | "not_found" | "busy" | "error", message? }], deleted: <count> } |
runtime is { status: "idle" | "running" | "requires_action" | "budget_paused", stopReason?, lastEventSeq?, pendingActionIds?, activeRunId? }. A runtime or budget that is not well-formed is dropped by the client.
Messages and turns
| Request | Body | Answer |
|---|---|---|
GET /conversations/:id/messages | ?limit&offset&order=asc|desc | { messages, total, limit, offset, hasMore } |
POST /conversations/:id/messages | { message, files?, fileIds?, metadata?, stream? } | JSON { message, response, status?, pendingActions?, stopReason?, budget?, spentUsd?, warnings?, conversationTitle?, usage? }, or with stream: true a Server-Sent Events stream |
POST /conversations/:id/messages/:messageId/regenerate | { stream? } | Like the line above, for the newest answer |
POST /conversations/:id/messages/:messageId/edit | { content, fileIds?, stream? } | Like the line above; later messages are dropped |
POST /conversations/:id/messages/:messageId/feedback | { rating: "up" | "down" | null, reasons?, comment? } | { feedback? } with the stored rating |
POST /conversations/:id/actions/:actionId | { resolution, stream? } | The continuation, JSON or stream |
POST /conversations/:id/continue | {} or { stream: true } | The continuation of a turn the usage limit stopped, JSON or stream |
POST /conversations/:id/cancel | none | 200 (or 202 while the run winds down): { cancelled, runId?, settled?, cancelledActionIds?, stopReason?, runtime? }. Nothing to cancel is cancelled: false, not an error |
An inline file in files is { name, content: <base64>, mimeType }; fileIds names files stored earlier with POST /files. The comment of a rating is at most 1000 characters in the UI.
A resolution is one of:
{ "type": "tool_approval", "decision": "allow", "approvedArgs": { "command": "ls -la" }, "rememberForSession": true }
{ "type": "tool_approval", "decision": "deny", "comment": "not that site" }
{ "type": "user_question", "answers": { "Which region?": "eu" } }
{ "type": "user_question", "answers": {}, "cancelled": true }
{ "type": "client_tool", "output": { "opened": 7 } }
{ "type": "client_tool", "error": "The host page did not answer" }The stream
A streamed answer is text/event-stream; each data: line is a JSON event with a type, a timestamp and, on agent-server 0.3.0+, a seq:
data: {"type":"stream.start","conversationId":"c1","messageId":"m2","timestamp":1760000000000,"seq":12}
data: {"type":"stream.text","text":"Hello","timestamp":1760000000100,"seq":13}
data: {"type":"stream.done","conversationId":"c1","messageId":"m2","content":"Hello there","stopReason":"end_turn","timestamp":1760000000900,"seq":19}The event types are stream.start, stream.text, stream.thinking, stream.tool_call, stream.tool_result, stream.progress, stream.action_required, stream.action_resolved, stream.error and stream.done; see Streaming. The reader accepts event: lines, CRLF line endings, multi-line data: and : comment lines (keep-alives).
Event log (reattach)
| Request | Answer |
|---|---|
GET /conversations/:id/events/stream?after=<seq>&types=a,b | A Server-Sent Events stream: the events after seq are replayed, then new ones arrive live. While a run is active the first frame is agent.message.snapshot (no id: line). agent.message.delta frames are live-only (no seq). Sent with Accept: text/event-stream |
The client reopens a closed stream from the last seq with backoff. A 4xx answer other than 408 and 429 is final; 408, 429, 5xx and network errors are retried. See Running turns for the frames.
Export
| Request | Answer |
|---|---|
GET /conversations/:id/export?format=md|json|pdf|html&tools=0|1 | The conversation as a file, with Content-Disposition naming it. A pdf request answered 501 export_format_unsupported is repeated as format=html (a print-friendly text/html page the chat prints) |
Chat falls back to building Markdown and JSON from the messages on screen when the route answers 404, 405 or 501.
Files
| Request | Answer |
|---|---|
POST /files?name=&conversationId=&mimeType= with the file's bytes as the body and its own Content-Type | { file }. A JSON file goes up as application/octet-stream with mimeType in the query |
POST /files with { file: { name, content, mimeType }, conversationId?, metadata? } | { file } (the JSON form) |
GET /files/:id | { file } |
DELETE /files/:id | Removes the file |
GET /files/:id/content?disposition=inline|attachment | The bytes |
GET /conversations/:id/files?limit=200 | { files, total, maxFiles?, hasMore? }, oldest first |
A file record is { id, name, mimeType, size, url?, expiresAt?, conversionStatus? }; conversionStatus is also read from conversion.status.
Embed session
Served by the portal for EmbedChat; not part of AgentServerClient:
| Request | Answer |
|---|---|
POST <sessionUrl> with { token } and no cookies | 200 { sessionToken, expiresAt, user?: { name, email } }, or 4xx { error: { code, message, reference } } |
DELETE <sessionUrl> with Authorization: Bearer <sessionToken> | Revokes the session |
All chat requests of an embedded chat then carry Authorization: Bearer <sessionToken>. See Embedding the Chat.

