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:

  1. model — which LLM runs it, as a Cotera model slug (for example azure-gpt-5.5 or claude-sonnet-5). Discover available models in the app's model selector.
  2. editorState — the agent's instructions, as a document (see below).
  3. Tools — the integrations the agent may call, attached inside editorState as 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:

FieldRequiredDescription
editorStateyesInstructions document (see above)
modelyesModel slug, e.g. azure-gpt-5.5
emojinoEmoji for the agent card
folderIdnoFolder 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's key from 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:

  1. Poll status until the run is done:

    GET /api/v1/resource/chat/{conversationId}/status
    

    The response has a stackFrames array. Each frame has depth (number), fpType (frame type), and status. Watch the frames where depth === 0: status is one of running, stalled (transient — still working), completed, failed, or cancelled. The run is done when every depth === 0 frame is in a terminal state (completed, failed, or cancelled). Note that a depth === 0 frame may report stalled while its child frames are still finishing; keep polling until it reaches a terminal state.

  2. Read messages:

    GET /api/v1/resource/chat/{conversationId}/list
    

    The response is an object: { "conversation": {...}, "messages": [...] }. Take the last entry of messages. The reply text lives at entry.message.parts — an array that contains bookkeeping entries (like { "type": "step-start" }) as well as text. Filter to part.type === 'text' and concatenate part.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

PurposeMethod & path
List tools (get keys)GET /api/v1/tools-v2
Create / replace agentPOST /api/v1/resource/agent/{id}
Update agentPOST /api/v1/resource/agent/{id}/update
Check attached toolsGET /api/v1/resource/agent/{id}/mentioned-tools-status
Run once (blocking)POST /api/v1/resource/agent/{id}/invoke
Run a batchPOST /api/v1/resource/agent/{id}/chat/batch
Poll a runGET /api/v1/resource/chat/{id}/status
Read a run's messagesGET /api/v1/resource/chat/{id}/list