# Agent API reference

> Build, run, and batch Cotera agents over HTTP: authentication, the agent object, tools, and worked examples.

Source: https://cotera.co/docs/reference/api

---

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](#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:

```js
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:

```json
{
  "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 |

```bash
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:

```json
{
  "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:

```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.

```bash
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).

```bash
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`:**

   ```js
   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:

```js
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`                    |

