Agent API reference
Everything you can do with an agent in the app, you can do over HTTP: create an agent, give it tools, run it once, or run it across a batch of inputs. This page is the complete guide to the agent endpoints, with examples you can copy and run.
Base URL: https://app.cotera.co/api/v1
Authentication
Every request takes a Bearer token. Create an API key in Settings → API Keys, then send it as:
Authorization: Bearer YOUR_API_KEY
The key is scoped to your organization; agents you create and run belong to that org.
The agent object
An agent has three things that matter:
model— which LLM runs it, as a Cotera model slug (for exampleazure-gpt-5.5orclaude-sonnet-5). Discover available models in the app's model selector.editorState— the agent's instructions, as a document (see below).- Tools — the integrations the agent may call, attached inside
editorStateas mention nodes (see Giving an agent tools).
editorState: instructions as a document
An agent's instructions are stored as a rich document, not a plain string. The format is a small JSON tree: a doc with paragraph nodes, each holding text nodes.
To turn a plain instruction string into editorState, wrap each line:
function instructions(text) {
return {
type: 'doc',
content: text.split('\n').map((line) => ({
type: 'paragraph',
content: line ? [{ type: 'text', text: line }] : []
}))
}
}
So the instruction "Reply only with JSON." becomes:
{
"type": "doc",
"content": [
{ "type": "paragraph", "content": [{ "type": "text", "text": "Reply only with JSON." }] }
]
}
Create an agent
You choose the agent's UUID and PUT the agent to that id (create is idempotent — the same id updates the same agent).
POST /api/v1/resource/agent/{id}
Body:
| Field | Required | Description |
|---|---|---|
editorState | yes | Instructions document (see above) |
model | yes | Model slug, e.g. azure-gpt-5.5 |
emoji | no | Emoji for the agent card |
folderId | no | Folder UUID to place the agent in |
curl -X POST https://app.cotera.co/api/v1/resource/agent/$AGENT_ID \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d '{
"model": "azure-gpt-5.5",
"editorState": { "type": "doc", "content": [
{ "type": "paragraph", "content": [{ "type": "text",
"text": "You classify companies. Reply ONLY with JSON: {\"category\":\"b2b\"|\"b2c\"|\"unknown\"}." }] }
] }
}'
Giving an agent tools
Tools are attached by referencing them inside editorState as mention nodes. Two steps:
1. Find the tool's key
GET /api/v1/tools-v2
Returns your org's tools. Each has a key (for example google/search) — that key is what you reference. The fields you need are key, name, description, and canExecute. Only attach tools where canExecute is true; a tool with canExecute: false isn't connected for your org and will silently do nothing at run time.
2. Add a mention node for the tool
A mention node looks like this — put it in a paragraph, alongside the text that tells the agent to use it:
{
"type": "mention",
"attrs": {
"referenceId": "google/search",
"label": "Google Search",
"id": "Google Search",
"data": "{\"t\":\"tool\"}"
}
}
referenceId— the tool'skeyfrom step 1. This is what actually attaches the tool.label/id— the tool's display name (used for rendering).data— must be the literal string{"t":"tool"}so the agent knows this mention is a tool.
Full example — an agent that searches the web and returns JSON:
{
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{ "type": "text", "text": "Research the company. Use the " },
{
"type": "mention",
"attrs": {
"referenceId": "google/search",
"label": "Google Search",
"id": "Google Search",
"data": "{\"t\":\"tool\"}"
}
},
{
"type": "text",
"text": " tool to find recent news, then reply ONLY with JSON: {\"headline\":\"...\",\"summary\":\"...\"}."
}
]
}
]
}
Verify the tools attached
GET /api/v1/resource/agent/{id}/mentioned-tools-status
Returns each attached tool with canExecute (whether it's ready to run). Use this to confirm your mention nodes worked before running the agent.
Run an agent once
POST /api/v1/resource/agent/{id}/invoke
Blocks until the agent finishes and returns its reply as text.
curl -X POST https://app.cotera.co/api/v1/resource/agent/$AGENT_ID/invoke \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d '{ "prompt": "Stripe" }'
# → { "success": true, "result": "{\"category\":\"b2b\"}" }
Optional body fields: model (override the agent's model for this run only), systemSuffix (extra system-prompt text for this run only).
Run a batch
POST /api/v1/resource/agent/{id}/chat/batch
Takes an array of 1–50 messages, starts them in parallel, and returns a conversationId for each. Results are fetched separately (below).
curl -X POST https://app.cotera.co/api/v1/resource/agent/$AGENT_ID/chat/batch \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d '[ {"message":"Notion"}, {"message":"Duolingo"}, {"message":"Databricks"} ]'
# → { "success": true, "conversationIds": ["…","…","…"] }
Read batch results
Every endpoint here returns HTTP 201 on success. For each conversationId:
-
Poll status until the run is done:
GET /api/v1/resource/chat/{conversationId}/statusThe response has a
stackFramesarray. Each frame hasdepth(number),fpType(frame type), andstatus. Watch the frames wheredepth === 0:statusis one ofrunning,stalled(transient — still working),completed,failed, orcancelled. The run is done when everydepth === 0frame is in a terminal state (completed,failed, orcancelled). Note that adepth === 0frame may reportstalledwhile its child frames are still finishing; keep polling until it reaches a terminal state. -
Read messages:
GET /api/v1/resource/chat/{conversationId}/listThe response is an object:
{ "conversation": {...}, "messages": [...] }. Take the last entry ofmessages. The reply text lives atentry.message.parts— an array that contains bookkeeping entries (like{ "type": "step-start" }) as well as text. Filter topart.type === 'text'and concatenatepart.text:const { messages } = await res.json() const last = messages.at(-1) const reply = (last.message.parts ?? []) .filter((p) => p.type === 'text') .map((p) => p.text) .join('')
Full worked example
Create a tool-wired agent, run it once, then batch it — end to end:
const BASE = 'https://app.cotera.co/api/v1'
const H = { Authorization: `Bearer ${process.env.API_KEY}`, 'Content-Type': 'application/json' }
const api = async (m, p, b) => {
const r = await fetch(`${BASE}${p}`, { method: m, headers: H, body: b && JSON.stringify(b) })
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`)
return r.json()
}
const id = crypto.randomUUID()
// 1. create with a tool mention
await api('POST', `/resource/agent/${id}`, {
model: 'azure-gpt-5.5',
editorState: {
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{ type: 'text', text: 'Research the company. Use the ' },
{
type: 'mention',
attrs: {
referenceId: 'google/search',
label: 'Google Search',
id: 'Google Search',
data: '{"t":"tool"}'
}
},
{
type: 'text',
text: ' tool to find recent news, then reply ONLY with JSON {"headline","summary"}.'
}
]
}
]
}
})
// 2. run once
console.log(await api('POST', `/resource/agent/${id}/invoke`, { prompt: 'Anthropic' }))
// 3. batch
const { conversationIds } = await api(
'POST',
`/resource/agent/${id}/chat/batch`,
['Stripe', 'Notion', 'Databricks'].map((message) => ({ message }))
)
for (const cid of conversationIds) {
while (true) {
const st = await api('GET', `/resource/chat/${cid}/status`)
const roots = (st.stackFrames ?? []).filter((f) => f.depth === 0)
if (roots.length && roots.every((f) => ['completed', 'failed', 'cancelled'].includes(f.status)))
break
await new Promise((r) => setTimeout(r, 2000))
}
const { messages } = await api('GET', `/resource/chat/${cid}/list`)
const last = messages.at(-1)
const text = (last.message.parts ?? [])
.filter((p) => p.type === 'text')
.map((p) => p.text)
.join('')
console.log(cid, text)
}
Endpoint summary
| Purpose | Method & path |
|---|---|
| List tools (get keys) | GET /api/v1/tools-v2 |
| Create / replace agent | POST /api/v1/resource/agent/{id} |
| Update agent | POST /api/v1/resource/agent/{id}/update |
| Check attached tools | GET /api/v1/resource/agent/{id}/mentioned-tools-status |
| Run once (blocking) | POST /api/v1/resource/agent/{id}/invoke |
| Run a batch | POST /api/v1/resource/agent/{id}/chat/batch |
| Poll a run | GET /api/v1/resource/chat/{id}/status |
| Read a run's messages | GET /api/v1/resource/chat/{id}/list |