
# AI Agents API

An **AI Agent** is the brain behind your bot: its instructions, personality, language, knowledge and tools. You build an Agent once and then point traffic at it. This guide covers everything you can do with an Agent over the API — create it, configure it, give it knowledge and tools, review its drafts, and route conversations to it.

- **Base URL** — `https://api.youraiconnector.com/v1`
- **Authentication** — your API key (see [Authentication](authentication.md))
- **Errors & paging** — see [Errors & Pagination](errors-and-pagination.md)

All examples below show the `?apiKey=` query form in cURL and the `X-API-Key` header in JavaScript and Python — either works on every endpoint.

If you are new to Agents as a concept, read [AI Agents](../ai-agents/ai-agents.md) first.


---

## How an Agent fits together

Four things are managed separately, and it helps to know which is which before you start:

| Piece | What it is | Where you set it |
|---|---|---|
| **Configuration** | Instructions, rules, goal, personality, language, AI tier, booking and follow-up behaviour | `PUT /agents/{agentId}` or the narrower `PUT /agents/{agentId}/bot-config` |
| **Knowledge** | FAQs and knowledge sources (pages and documents the platform has read for you) | [FAQs API](faqs.md) and `POST /agents/{agentId}/kb-sources` |
| **Tools** | Custom functions and MCP servers the Agent may call mid-conversation | `POST /agents/{agentId}/custom-functions` and `POST /agents/{agentId}/mcp-servers` |
| **Routing** | Which channels and conversations actually reach this Agent | Entry Points — `PUT /entry-points/channel-defaults` and `POST /agents/{agentId}/entry-points` |

> **A new Agent answers nobody until you route to it.** Creating an Agent does not put it on a channel. That is the step most integrations miss — see [Routing conversations to an Agent](#routing-conversations-to-an-agent) at the end of this page.

---

## The Agent object

A full Agent document is large — several hundred kilobytes, mostly its FAQ list, its knowledge sources and any page content read from your website. Because of that, listing returns a short **summary row** per Agent when you ask for it:

```json
{
  "id": "ag7HkQ2ZpLxR3mNb",
  "name": "Listing assistant",
  "active": true,
  "language": "en",
  "goal": "Book a viewing",
  "tags": [],
  "anthropic_model": "standard",
  "ai_speed": "balanced",
  "enable_bookings": false,
  "enable_follow_ups": true,
  "faq_refs_count": 42,
  "kb_source_refs_count": 3,
  "created_at": 1700000000000,
  "last_modified_at": 1700000000000
}
```

| Field | Type | Description |
|---|---|---|
| `id` | string | The Agent's unique identifier. |
| `name` | string \| null | Agent name, as shown in the dashboard. |
| `active` | boolean \| null | Whether the Agent is currently allowed to reply. |
| `language` | string \| null | Language the Agent replies in. |
| `goal` | string \| null | What the Agent works towards, shortened to the first 200 characters (a trailing ellipsis means it was shortened). |
| `tags` | array \| null | The Agent's tagging rules. |
| `anthropic_model` | string \| null | AI quality tier: `standard`, `economy`, `max` or `mini`. |
| `ai_speed` | string \| null | How much reasoning the Agent applies before replying: `fast`, `fast_thinker`, `balanced` or `thorough`. |
| `enable_bookings` | boolean \| null | Whether the Agent may book appointments. |
| `enable_follow_ups` | boolean \| null | Whether the Agent sends follow-up messages. |
| `faq_refs_count` | integer | How many FAQs are in this Agent's knowledge base. |
| `kb_source_refs_count` | integer | How many knowledge sources are linked to it. |
| `created_at` | integer \| null | Creation time, epoch milliseconds. |
| `last_modified_at` | integer \| null | Last change, epoch milliseconds. |

The full document adds everything else: `instructions`, `rules`, `personality`, `availability`, `follow_up_config`, the linked FAQ and knowledge-source lists, the generated prose blocks, and any run state (`tag_generation`, `optimize_run`).

> Some responses also carry `substrate_campaign_id`. It is an internal record kept on older accounts; you never need to act on it, and on newer accounts it is `null` or absent.

---

## List Agents

`GET /agents` — every Agent on the account, newest first.

This endpoint is **not paged**. By default each Agent comes back with its full configuration, which is heavy: a single Agent can reach 580 KB and a 64-Agent account over 3 MB. Pass `view=summary` for a short row per Agent instead, then read the one you want with [Get an Agent](#get-an-agent).

**Query parameters**

| Parameter | Description |
|---|---|
| `view` | Set to `summary` for short rows. Any other value returns `400`. Omit for full documents. |
| `fields` | Only applies together with `view=summary`. Comma-separated summary keys to keep, for example `id,name,active`. `id` is always included; unknown names are ignored. |

**cURL**

```bash
curl "https://api.youraiconnector.com/v1/agents?apiKey=YOUR_API_KEY&view=summary&fields=id,name,active"
```

**JavaScript**

```javascript
const res = await fetch("https://api.youraiconnector.com/v1/agents?view=summary", {
  headers: { "X-API-Key": "YOUR_API_KEY" },
});
const { agents } = await res.json();
```

**Python**

```python
import requests

res = requests.get(
    "https://api.youraiconnector.com/v1/agents",
    headers={"X-API-Key": "YOUR_API_KEY"},
    params={"view": "summary"},
)
agents = res.json()["agents"]
```

**Response** (`200`)

```json
{
  "success": true,
  "agents": [
    { "id": "ag7HkQ2ZpLxR3mNb", "name": "Listing assistant", "active": true }
  ]
}
```

---

## Create an Agent

`POST /agents` — only `name` is really needed; send whatever configuration you already know alongside it. A new Agent is active by default.

**Request fields** (all optional except `name`)

| Field | Type | Description |
|---|---|---|
| `name` | string | Agent name. |
| `active` | boolean | Whether it may reply straight away. Defaults to `true`. |
| `language` | string | Language the Agent replies in. |
| `instructions` | string | Primary instructions that steer how it talks to contacts. |
| `rules` | string | Hard rules it must always follow. |
| `goal` | string | The outcome it should work towards. |
| `personality` | string | Tone of voice and personality. |
| `availability` | object | Active hours per weekday — see [Set active hours](#set-active-hours). |
| `ai_speed` | string | `fast`, `fast_thinker`, `balanced` or `thorough`. |
| `anthropic_model` | string | `standard`, `economy`, `max` or `mini`. |
| `scrape_urls` | string[] | Pages to read and build the Agent's instructions from. |

**Building an Agent from your website.** Include `scrape_urls` and the platform reads those pages and writes the instructions for you. The response tells you whether that generation started, so you know whether to poll the Agent for progress.

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/agents?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Listing assistant",
    "language": "en",
    "instructions": "Answer questions about our listings and book viewings.",
    "goal": "Book a viewing"
  }'
```

**JavaScript**

```javascript
const res = await fetch("https://api.youraiconnector.com/v1/agents", {
  method: "POST",
  headers: { "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json" },
  body: JSON.stringify({
    name: "Listing assistant",
    scrape_urls: ["https://example.com", "https://example.com/faq"],
  }),
});
const data = await res.json();
console.log(data.agent_id);
```

**Python**

```python
res = requests.post(
    "https://api.youraiconnector.com/v1/agents",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={"name": "Listing assistant", "scrape_urls": ["https://example.com"]},
)
print(res.json()["agent_id"])
```

**Response** (`201`)

```json
{
  "success": true,
  "agent_id": "ag7HkQ2ZpLxR3mNb",
  "substrate_campaign_id": null,
  "agent_generation_queued": true
}
```

`agent_generation_queued` is `true` when the platform started writing the instructions from the pages you supplied.

A `400` means the body was not a JSON object, a field was rejected, or the Agent exceeds the configuration size your plan allows. A `403` means the account is not allowed to use one of the settings you sent — for example an AI tier its account provider has not granted.

---

## Get an Agent

`GET /agents/{agentId}`

Pass `fields` with a comma-separated list to get back only what you need, for example `fields=name,active,goal`. The `id` is always included, and names that do not exist on the Agent are ignored rather than rejected. Omit it to get the whole document.

**cURL**

```bash
curl "https://api.youraiconnector.com/v1/agents/ag7HkQ2ZpLxR3mNb?apiKey=YOUR_API_KEY&fields=name,active,goal"
```

**JavaScript**

```javascript
const res = await fetch("https://api.youraiconnector.com/v1/agents/ag7HkQ2ZpLxR3mNb?fields=name,active", {
  headers: { "X-API-Key": "YOUR_API_KEY" },
});
const { agent } = await res.json();
```

**Python**

```python
res = requests.get(
    "https://api.youraiconnector.com/v1/agents/ag7HkQ2ZpLxR3mNb",
    headers={"X-API-Key": "YOUR_API_KEY"},
    params={"fields": "name,active"},
)
agent = res.json()["agent"]
```

An Agent that does not exist on your account returns `404`.

---

## Update an Agent

`PUT /agents/{agentId}` — send only the fields you want to change; everything else is left untouched.

Nested settings can be addressed leaf by leaf with a dotted key, so `"availability.monday"` changes just Monday and leaves the rest of the week alone.

**Notes**

- To change which bookable event type the Agent books into, send `event_id` (the event's id, or `null` to clear it). Send `event_ids` with an array to link several at once — the first becomes the primary and `[]` unlinks everything. `event_id` and `event_ids` are mutually exclusive, and the `event` field itself cannot be written directly.
- `enable_bookings` must be a real boolean, and `booking_provider` must be one of `default`, `zenchef`, `formitable`.
- Ownership and identity fields are ignored, as is internal run state (generation and optimisation progress).
- **Routing is not set here.** Use `PUT /entry-points/channel-defaults` to make the Agent the answerer for a channel, `POST /agents/{agentId}/entry-points` for keyword and comment rules, and `PATCH /agents/{agentId}/active` to pause or resume it.

**cURL**

```bash
curl -X PUT "https://api.youraiconnector.com/v1/agents/ag7HkQ2ZpLxR3mNb?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "instructions": "Answer questions about our listings and always offer a viewing.",
    "anthropic_model": "standard"
  }'
```

**JavaScript**

```javascript
await fetch("https://api.youraiconnector.com/v1/agents/ag7HkQ2ZpLxR3mNb", {
  method: "PUT",
  headers: { "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json" },
  body: JSON.stringify({ "availability.monday": { start_time: "09:00", end_time: "17:00" } }),
});
```

**Python**

```python
requests.put(
    "https://api.youraiconnector.com/v1/agents/ag7HkQ2ZpLxR3mNb",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={"goal": "Book a viewing within three messages"},
)
```

**Response** (`200`)

```json
{ "success": true, "agent_id": "ag7HkQ2ZpLxR3mNb" }
```

An empty body returns `400` with `"No fields to update"`.

---

## Update bot settings

`PUT /agents/{agentId}/bot-config` — the narrow way to change just the conversational settings.

An Agent has no separate bot section: its settings sit directly on the Agent, so the field names here are the same ones you would send to `PUT /agents/{agentId}`. This endpoint exists as the safe, focused way to change a handful of them. At least one field is required.

| Field | Description |
|---|---|
| `instructions` | Primary instructions that steer how the Agent talks to contacts. |
| `rules` | Hard rules it must always follow. |
| `goal` | The outcome it should work towards in each conversation. |
| `personality` | Tone-of-voice and personality description. |
| `language` | Language the Agent replies in. |
| `ai_speed` | `fast`, `fast_thinker`, `balanced` or `thorough`. |
| `anthropic_model` | `standard`, `economy`, `max` or `mini`. |
| `max_messages` | Maximum number of Agent messages per conversation. |
| `alert_human_when` | When the Agent should alert a human teammate. |
| `ai_transparency` | Whether the Agent discloses that it is an AI. |

> **Field names must be plain names here** — letters, numbers, underscores and hyphens. Dotted paths are not accepted on this endpoint (unlike `PUT /agents/{agentId}`), so `bot.goal` is rejected with a `400`.

```bash
curl -X PUT "https://api.youraiconnector.com/v1/agents/ag7HkQ2ZpLxR3mNb/bot-config?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "goal": "Book a viewing within three messages", "ai_speed": "thorough" }'
```

Long text counts against the configuration size your plan allows, so a very large instruction set can be refused with a `400`.

---

## Set active hours

`PUT /agents/{agentId}/active-hours` — the hours during which the Agent replies automatically. Outside those windows it stays quiet.

Send an `availability` object keyed by weekday (`monday` through `sunday`). Each day takes a single time window or a list of windows, in 24-hour `HH:MM` form. Days you leave out keep whatever they had, and any key that is not a weekday is rejected — so a typo cannot silently do nothing.

```bash
curl -X PUT "https://api.youraiconnector.com/v1/agents/ag7HkQ2ZpLxR3mNb/active-hours?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "availability": {
      "monday": { "start_time": "09:00", "end_time": "17:00" },
      "tuesday": [
        { "start_time": "09:00", "end_time": "12:00" },
        { "start_time": "13:00", "end_time": "17:00" }
      ]
    }
  }'
```

**Response** (`200`)

```json
{ "success": true, "agent_id": "ag7HkQ2ZpLxR3mNb" }
```

A bad weekday key returns `400`: `"Invalid availability keys: funday. Allowed keys: monday through sunday."`

---

## Pause or resume an Agent

`PATCH /agents/{agentId}/active` — turns the Agent on or off. A paused Agent keeps all of its configuration but stops replying immediately; resuming takes effect straight away.

```bash
curl -X PATCH "https://api.youraiconnector.com/v1/agents/ag7HkQ2ZpLxR3mNb/active?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "active": false }'
```

```javascript
await fetch("https://api.youraiconnector.com/v1/agents/ag7HkQ2ZpLxR3mNb/active", {
  method: "PATCH",
  headers: { "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json" },
  body: JSON.stringify({ active: false }),
});
```

**Response** (`200`)

```json
{ "success": true, "agent_id": "ag7HkQ2ZpLxR3mNb", "active": false }
```

`active` must be a real boolean — anything else returns `400` with `"active (boolean) is required"`.

---

## Duplicate an Agent

`POST /agents/{agentId}/duplicate` — creates a copy with its configuration preserved. The copy sends nothing until you point a channel or an Entry Point at it.

```bash
curl -X POST "https://api.youraiconnector.com/v1/agents/ag7HkQ2ZpLxR3mNb/duplicate?apiKey=YOUR_API_KEY"
```

**Response** (`201`)

```json
{ "success": true, "agent_id": "ag9WsX3cRfV6tGyH", "source_agent_id": "ag7HkQ2ZpLxR3mNb" }
```

A duplicate counts against your plan's Agent allowance exactly like creating one from scratch, so it is refused with `403` when the account is at its limit.

---

## Delete an Agent

`DELETE /agents/{agentId}`

The delete is refused while the Agent is still attached to something that would stop working without it — a broadcast, an Entry Point, or (on older accounts) a campaign. The response lists what is holding it so you can detach those first and retry.

```bash
curl -X DELETE "https://api.youraiconnector.com/v1/agents/ag7HkQ2ZpLxR3mNb?apiKey=YOUR_API_KEY"
```

**Response** (`200`)

```json
{ "success": true, "agent_id": "ag7HkQ2ZpLxR3mNb" }
```

**Blocked** (`409`)

```json
{
  "success": false,
  "error": "Agent is still attached to one or more broadcast(s). Detach it first.",
  "blocking_campaign_ids": [],
  "blocking_broadcast_ids": ["bc5TgYhUj8IkOlPm"],
  "blocking_entry_point_ids": []
}
```

---

## Drafts: review changes before they go live

Edits made in the editor, and any rewrite produced by [Optimize with AI](#optimize-an-agent-with-ai), are held as an **unpublished draft** until you publish them. The live Agent keeps answering with its current configuration until then.

### Publish the draft

`POST /agents/{agentId}/publish-draft` — moves the draft onto the live configuration and clears the draft in the same step.

```bash
curl -X POST "https://api.youraiconnector.com/v1/agents/ag7HkQ2ZpLxR3mNb/publish-draft?apiKey=YOUR_API_KEY"
```

**Response** (`200`)

```json
{ "success": true, "agent_id": "ag7HkQ2ZpLxR3mNb", "published_keys": ["instructions", "goal"] }
```

`published_keys` lists the settings that moved from the draft onto the live Agent, so you can show what changed.

> **Check that a draft exists before calling this.** Publishing an Agent that has no draft is not a supported call and currently comes back as a `500` with a generic message, not a specific one. To throw a draft away instead, use discard below.

### Discard the draft

`POST /agents/{agentId}/discard-draft` — throws the draft away and leaves the live configuration exactly as it is. Safe to call when there is no draft; nothing happens.

```bash
curl -X POST "https://api.youraiconnector.com/v1/agents/ag7HkQ2ZpLxR3mNb/discard-draft?apiKey=YOUR_API_KEY"
```

---

## Optimize an Agent with AI

`POST /agents/{agentId}/optimize` — rewrites the Agent's configuration from your feedback ("it keeps offering discounts", "answers are too long") and saves the rewrite **as a draft** rather than putting it live.

Send either `user_feedback` (a plain instruction) or, when reacting to a specific bad reply, `thumbs_down_feedback` together with the offending `thumbs_down_message`. At least one of the two must carry text.

```bash
curl -X POST "https://api.youraiconnector.com/v1/agents/ag7HkQ2ZpLxR3mNb/optimize?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "user_feedback": "Keep replies under three sentences." }'
```

**Response** (`202`)

```json
{ "success": true, "agent_id": "ag7HkQ2ZpLxR3mNb" }
```

The work runs in the background and the call returns straight away. Read the Agent with `GET /agents/{agentId}` and watch `optimize_run.status`; once it is back to `Draft`, the rewrite is waiting as the Agent's draft. Review it, then either publish it or discard it.

Only one run at a time per Agent — a second call while one is in flight returns `409`. This uses AI credits.

---

## Tagging rules

A tagging rule is a tag plus a description of when it applies. During a conversation the Agent reads that description and tags the contact when it fits, which is how tag-driven automations get triggered.

**The rule object**

| Field | Required | Description |
|---|---|---|
| `name` | Yes | The tag to apply, for example `hot-lead`. |
| `description` | No | When the Agent should apply it, written as an instruction it follows. |
| `webhook` | No | URL called when the Agent applies this tag. |
| `ai_can_remove` | No | Whether the Agent may also take the tag off again. Defaults to `false`. |
| `tag_id` | No | Id of an existing tag on your account to link the rule to. Without it, the rule links to the tag with the same name, creating it if there is none — so every rule can be addressed by tag id afterwards. |

### Add a tagging rule

`POST /agents/{agentId}/tags`

```bash
curl -X POST "https://api.youraiconnector.com/v1/agents/ag7HkQ2ZpLxR3mNb/tags?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "tag": {
      "name": "hot-lead",
      "description": "Apply when the contact asks about pricing or wants to book a call.",
      "ai_can_remove": false
    }
  }'
```

**Response** (`200`)

```json
{ "success": true, "agent_id": "ag7HkQ2ZpLxR3mNb", "tag": { "name": "hot-lead", "...": "..." } }
```

### Replace a tagging rule

`PUT /agents/{agentId}/tags/{tagId}` — the rule is found by the tag id in the path and **replaced wholesale**, not merged, so send the full rule rather than only the part you are changing. The tag it points at is preserved even if you leave `tag_id` out, so an edit cannot detach the rule from its tag.

```bash
curl -X PUT "https://api.youraiconnector.com/v1/agents/ag7HkQ2ZpLxR3mNb/tags/tg8YuIoP2aSdF3gH?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "tag": { "name": "hot-lead", "description": "Apply only when the contact asks to book a call." } }'
```

### Remove a tagging rule

`DELETE /agents/{agentId}/tags/{tagId}` — the Agent stops applying that tag. The tag itself, and any contacts already carrying it, are untouched.

```bash
curl -X DELETE "https://api.youraiconnector.com/v1/agents/ag7HkQ2ZpLxR3mNb/tags/tg8YuIoP2aSdF3gH?apiKey=YOUR_API_KEY"
```

Both endpoints return `404` when the Agent does not exist **or** when it has no rule for that tag.

### Generate a tag set with AI

`POST /agents/{agentId}/tags/generate` — designs a whole set of rules (the tag names and the "apply when…" wording behind each) by reading the Agent's own instructions and goal.

| Field | Description |
|---|---|
| `mode` | `merge` (the default) keeps the rules already on the Agent and adds to them. `replace` designs the set from scratch. |

```bash
curl -X POST "https://api.youraiconnector.com/v1/agents/ag7HkQ2ZpLxR3mNb/tags/generate?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "mode": "merge" }'
```

**Response** (`202`)

```json
{ "success": true, "agent_id": "ag7HkQ2ZpLxR3mNb", "mode": "merge" }
```

The work runs in the background. Read the Agent and watch `tag_generation.status`; the rules themselves land in the Agent's `tags`. Only one run at a time per Agent (`409` otherwise), and it uses AI credits.

---

## Knowledge sources

Knowledge sources are the pages and documents the platform has read for you. Attaching one to an Agent lets it answer from that content.

**Where source ids come from.** Add content with the knowledge-base endpoints — `POST /kb-sources/url` for a page, `POST /kb-sources/file` for a document, `POST /kb-sources/bulk-import` for a whole site. Those return a `source_id` you poll with `GET /kb-sources/{sourceId}` until it is ready. `POST /kb-sources/url` also takes `autoLinkToAgentId`, which attaches the source to an Agent as soon as the import finishes, so you can skip the attach call below.

### Attach knowledge sources

`POST /agents/{agentId}/kb-sources` — send `kb_source_ids` with a list to attach a whole set in one call (what you want after crawling a site), or `kb_source_id` for a single one. Send one or the other. Attaching something already attached changes nothing.

```bash
curl -X POST "https://api.youraiconnector.com/v1/agents/ag7HkQ2ZpLxR3mNb/kb-sources?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "kb_source_ids": ["kb2QwErTyUi9OpAs", "kb6ZxCvBnM4kLjHg"] }'
```

**Response** (`200`)

```json
{
  "success": true,
  "agent_id": "ag7HkQ2ZpLxR3mNb",
  "kb_source_id": "kb2QwErTyUi9OpAs",
  "kb_source_ids": ["kb2QwErTyUi9OpAs", "kb6ZxCvBnM4kLjHg"]
}
```

### Detach knowledge sources

`DELETE /agents/{agentId}/kb-sources/{kbSourceId}` for one, or `POST /agents/{agentId}/kb-sources/bulk-remove` with `kb_source_ids` for several. Bulk removal is a `POST` because the list of ids travels in the body.

```bash
curl -X POST "https://api.youraiconnector.com/v1/agents/ag7HkQ2ZpLxR3mNb/kb-sources/bulk-remove?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "kb_source_ids": ["kb2QwErTyUi9OpAs"] }'
```

The sources themselves are not deleted and stay available to your other Agents. Detaching something that is not attached changes nothing.

### FAQs

FAQs are managed on their own endpoints and linked to an Agent from there: `POST /faqs/{faqId}/link` with `{ "agent_id": "ag7HkQ2ZpLxR3mNb" }`, and `POST /faqs/{faqId}/unlink` to take it away again. A FAQ can be shared by any number of Agents. See the [FAQs API](faqs.md).

> A FAQ is only used by the Agents it is linked to — creating one is not enough on its own.

---

## Tools

### Custom functions

`POST /agents/{agentId}/custom-functions` lets the Agent call one of your custom functions during conversations. Only functions belonging to the same account can be attached, and attaching one that is already attached changes nothing.

```bash
curl -X POST "https://api.youraiconnector.com/v1/agents/ag7HkQ2ZpLxR3mNb/custom-functions?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "custom_function_id": "cf7Hk2ZpLxR3mNbV" }'
```

`DELETE /agents/{agentId}/custom-functions/{customFunctionId}` detaches it. The function itself is not deleted and stays available to your other Agents.

Manage the functions themselves on `/custom-functions` — see [Custom Functions](../ai-automation/custom-functions.md) for what they are.

### MCP servers

An MCP server is a ready-made bundle of tools your Agent can discover and call on its own — see [Connect MCP Servers to Your Bot](../ai-automation/mcp-servers.md). Servers are registered once on the account, then attached to whichever Agents should use them.

> MCP servers need the **custom functions** feature on your plan. Without it, the account-level `/mcp-servers` endpoints return `403`. Attaching an already-registered server to an Agent is not gated.

#### Register a server

`POST /mcp-servers`

| Field | Required | Description |
|---|---|---|
| `name` | Yes | A label for the server. |
| `url` | Yes | The server's address. Must be reachable over the public internet. |
| `auth_type` | No | `header` (the default) for a static auth header, or `oauth2`. |
| `auth_header_name` | No | Header to send the credential in. Defaults to `Authorization`. |
| `auth_header_value` | No | The credential itself. Never returned in any response. |
| `enabled` | No | Whether the server is available to Agents. Defaults to `true`. |
| `enabled_tools` | No | Allow-list of tool names. `null` means every tool the server offers is on. |
| `tool_policies` | No | Per-tool limits, keyed by tool name — how often a tool may fire, result caching, and a read-only override. Pass `null` to clear them all. |

```bash
curl -X POST "https://api.youraiconnector.com/v1/mcp-servers?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Inventory",
    "url": "https://tools.example.com/mcp",
    "auth_header_value": "Bearer sk_live_xxx"
  }'
```

**Response** (`201`)

```json
{
  "success": true,
  "server_id": "ms4TgBnH7yUj2kLp",
  "tools": [{ "name": "check_stock", "description": "Look up stock for a SKU." }],
  "last_error": null,
  "server": { "server_id": "ms4TgBnH7yUj2kLp", "name": "Inventory", "...": "..." }
}
```

On save, the platform connects to the server and caches the list of tools it offers. **A server that cannot be reached still saves**, with the reason in `last_error` and an empty tool list — so you can register first and fix connectivity afterwards.

An `auth_type` of `oauth2` saves the registration with `oauth_connected: false` and no tools: there is no token yet. Authorising an OAuth server needs a browser sign-in and is done from the dashboard, not over the API.

#### List, update and delete servers

- `GET /mcp-servers` — every registered server, newest first, under `servers`.
- `PUT /mcp-servers/{serverId}` — send only what you want to change. Changing the URL or the auth fields re-tests the connection and refreshes the cached tool list.
- `DELETE /mcp-servers/{serverId}` — removes the registration and unlinks it from every Agent and campaign that had it enabled.

```bash
curl "https://api.youraiconnector.com/v1/mcp-servers?apiKey=YOUR_API_KEY"
```

**Secrets never come back.** Responses carry `auth_header_value_set` (a `true`/`false` flag saying a value is stored) instead of the credential, and OAuth tokens and client secrets stay server-side. Everything else is returned: `name`, `url`, `enabled`, `auth_type`, `auth_header_name`, `tools`, `enabled_tools`, `tool_policies`, `oauth_connected`, `tools_cached_at`, `last_connected_at`, `last_error`, `created_at`, `updated_at`.

#### Test a connection

`POST /mcp-servers/test-connection` — connects to a server and lists its tools. Two ways to call it:

- with `server_id` — tests the **saved** configuration and refreshes its cached tool list;
- with an inline `url` (plus `auth_header_name` / `auth_header_value`) — a pre-save test that stores nothing.

```bash
curl -X POST "https://api.youraiconnector.com/v1/mcp-servers/test-connection?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://tools.example.com/mcp", "auth_header_value": "Bearer sk_live_xxx" }'
```

**Response** (`200`)

```json
{
  "success": true,
  "server_name": "Inventory tools",
  "tools": [{ "name": "check_stock", "description": "Look up stock for a SKU." }]
}
```

A connection failure is **not** an HTTP error — you get a `200` with `success: false` and an `error` describing what went wrong, so you can show it next to the field the operator is editing.

#### Attach a server to an Agent

Registering a server does not give any Agent access to it. Attach it:

```bash
curl -X POST "https://api.youraiconnector.com/v1/agents/ag7HkQ2ZpLxR3mNb/mcp-servers?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "mcp_server_id": "ms4TgBnH7yUj2kLp" }'
```

**Response** (`200`)

```json
{ "success": true, "agent_id": "ag7HkQ2ZpLxR3mNb", "mcp_server_id": "ms4TgBnH7yUj2kLp" }
```

`DELETE /agents/{agentId}/mcp-servers/{mcpServerId}` detaches it again. The server itself is not deleted and stays available to your other Agents. Attaching or detaching something that is already in that state changes nothing.

---

## Media library

The media library holds the files an Agent may send during a conversation — a menu, a price list, a product photo. An Agent can hold at most **50 items**.

### List media

`GET /agents/{agentId}/media-library`

```bash
curl "https://api.youraiconnector.com/v1/agents/ag7HkQ2ZpLxR3mNb/media-library?apiKey=YOUR_API_KEY"
```

**Response** (`200`)

```json
{
  "success": true,
  "agent_id": "ag7HkQ2ZpLxR3mNb",
  "media_items": [
    {
      "id": "mi4RtY7uIoP1aSdF",
      "item_id": "mi4RtY7uIoP1aSdF",
      "media_home": "agent",
      "title": "Spring menu",
      "description": "Send when someone asks what is on the menu.",
      "ai_description": "A one-page menu listing seasonal dishes and prices.",
      "type": "document",
      "media_content_type": "application/pdf",
      "media_url": "https://storage.googleapis.com/...",
      "max_sends_per_conversation": 1,
      "created_at": 1700000000000
    }
  ]
}
```

Items stored on the Agent come first, then any older items still stored on the campaign the Agent was built from; `media_home` (`agent` or `campaign`) says which is which. Within each group the newest comes first.

> **`media_url` expires after 7 days.** It is the download link created when the file was uploaded — treat an old one as stale rather than broken, and re-read the list to get a fresh link.

### Upload media

`POST /agents/{agentId}/media-library` — the file is uploaded inline as base64, up to **10 MB**. The call returns once the file is stored, so allow a little longer than for a normal request. Note that this body uses camelCase field names.

| Field | Required | Description |
|---|---|---|
| `base64Data` | Yes | File contents, base64 encoded, without a data-URL prefix. |
| `mimeType` | Yes | MIME type of the file. |
| `fileName` | Yes | Original filename, used to name the stored file. |
| `title` | No | Short label shown in the library. |
| `description` | No | The "when should the Agent send this" instruction. |
| `sendMessage` | No | Preferred wording the Agent says when it sends the item. Trimmed to 500 characters. |
| `maxSendsPerConversation` | No | How many times it may be sent to the same contact in one conversation. Defaults to `1`. |
| `sendAsVoiceNote` | No | Audio uploads only — store the file as a WhatsApp voice note. Ignored for other file types. |

```bash
curl -X POST "https://api.youraiconnector.com/v1/agents/ag7HkQ2ZpLxR3mNb/media-library?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "base64Data": "JVBERi0xLjQKJcfs...",
    "mimeType": "application/pdf",
    "fileName": "spring-menu.pdf",
    "title": "Spring menu",
    "description": "Send when someone asks what is on the menu.",
    "maxSendsPerConversation": 1
  }'
```

Two things happen automatically: an animated GIF is converted to video so it plays on every channel, and the platform writes a short summary of what is actually in the file so the Agent knows when it fits.

A `400` covers missing fields, an unsupported file type, an empty or oversized file, and hitting the 50-item limit. A `403` means the media library is switched off for the account.

### Update a media item

`PATCH /agents/{agentId}/media-library/{itemId}` — metadata only. The file itself cannot be replaced; upload a new item and delete the old one. This body uses snake_case: `title`, `description`, `send_message`, `max_sends_per_conversation` (a non-negative whole number, or `null` to clear the limit).

```bash
curl -X PATCH "https://api.youraiconnector.com/v1/agents/ag7HkQ2ZpLxR3mNb/media-library/mi4RtY7uIoP1aSdF?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "title": "Summer menu", "max_sends_per_conversation": 2 }'
```

**Response** (`200`)

```json
{
  "success": true,
  "agent_id": "ag7HkQ2ZpLxR3mNb",
  "item_id": "mi4RtY7uIoP1aSdF",
  "campaign_id": "",
  "media_home": "agent"
}
```

### Delete a media item

`DELETE /agents/{agentId}/media-library/{itemId}` — removes the item and its stored file. Deleting an item that is already gone succeeds and reports `deleted: false`, so the call is safe to retry.

```bash
curl -X DELETE "https://api.youraiconnector.com/v1/agents/ag7HkQ2ZpLxR3mNb/media-library/mi4RtY7uIoP1aSdF?apiKey=YOUR_API_KEY"
```

---

## Generate follow-up messages

`POST /agents/{agentId}/template-generation` — writes the Agent's follow-up messages for you (the nudges it sends when a conversation goes quiet), based on what the Agent is for.

| Field | Description |
|---|---|
| `type` | `all` (the default) writes the whole set. `cold_only` writes only the messages for contacts who never replied. |

```bash
curl -X POST "https://api.youraiconnector.com/v1/agents/ag7HkQ2ZpLxR3mNb/template-generation?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "type": "all" }'
```

There are two ways this comes back, and the `target` field tells you which:

- **`target: "agent"` with a `200`** — the messages were written during the call and the result is in `data`. Read them back from the Agent's `follow_up_config`. This is the usual case.
- **`target: "campaign"` with a `202`** — the work was queued against the campaign named in `campaign_id`. Watch that campaign's `template_generation_status` until it finishes.

`cold_only` needs an outgoing campaign and is refused with `409` (`reason: "cold_only_requires_campaign"`) on an Agent that has none. A `403` means automatic follow-ups are not switched on for the account. This uses AI credits, and a `400` with `"Insufficient credits."` means the account is out.

---

## Routing conversations to an Agent

An Agent only answers the conversations an **Entry Point** sends it. Until a channel has one, a first message from someone you have never spoken to is still stored, but nothing picks it up and no assistant replies.

| What you want to do | Call |
|---|---|
| Make an Agent the answerer for a whole channel | `PUT /entry-points/channel-defaults` with `{ "channel": "instagram", "agent_id": "AGENT_ID" }` |
| Add a narrower rule (keywords, comments, new followers) | `POST /agents/{agentId}/entry-points` |
| See the rules pointing at one Agent | `GET /agents/{agentId}/entry-points` |
| Leave a channel with nobody answering | `DELETE /entry-points/channel-defaults?channel=instagram` |

### List an Agent's Entry Points

`GET /agents/{agentId}/entry-points` — the routing rules that send conversations to this Agent, newest first. Both current and retired rules come back; a retired one has `enabled: false`.

```bash
curl "https://api.youraiconnector.com/v1/agents/ag7HkQ2ZpLxR3mNb/entry-points?apiKey=YOUR_API_KEY"
```

For the whole account's channel defaults, including a channel deliberately set to nobody, read `GET /entry-points/channel-defaults` instead.

### Create an Entry Point

`POST /agents/{agentId}/entry-points` — the Agent in the path always wins, so a rule can never be created for a different Agent than the one in the URL.

| `type` | What it does |
|---|---|
| `channel_default` | The Agent answers every new contact on the listed channels. Prefer `PUT /entry-points/channel-defaults` for this — it retires the previous answerer for you, which creating a second default here does not. |
| `keyword` | The Agent takes over when the first message contains one of `match_config.keywords`. At least one keyword is required. |
| `instagram_comment` / `facebook_comment` | The Agent replies to comments on your posts. The matching channel must be listed in `channels`. |
| `instagram_follower` | The Agent greets new followers. |

`channels` is required and says which channels the rule covers — for example `whatsapp`, `whatsapp_web`, `instagram`, `messenger`, `telegram`, `sms`, `email`, `chat_widget` or `custom_channel`. New rules are enabled unless you say otherwise.

```bash
curl -X POST "https://api.youraiconnector.com/v1/agents/ag7HkQ2ZpLxR3mNb/entry-points?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "keyword",
    "channels": ["whatsapp", "instagram"],
    "match_config": { "keywords": ["pricing", "quote"] }
  }'
```

**Response** (`201`)

```json
{ "success": true, "entry_point_id": "ep3KmQ8vTzXr5nWd" }
```

**Which rule wins when several could:** an ongoing conversation or a manual assignment keeps the Agent it already has; otherwise keyword rules beat comment rules, which beat follower rules, and a channel default is the last resort. Whether these rules decide anything yet on an account is reported by `GET /entry-points/routing-status`.

This is the short version. The [Entry Points API](entry-points.md) guide covers the full ladder, comment and follower rules, one Agent per WhatsApp number, and changing or deleting a rule. See [Entry Points](../ai-agents/entry-points.md) for the concept, and the [Channels API](channels.md) for connecting the channel itself.

---

## AI Agents API errors

Agent endpoints return the standard error envelope:

```json
{
  "success": false,
  "error": "Agent not found"
}
```

| Status | When it happens on an Agent endpoint |
|---|---|
| `400` | A required field is missing or invalid — an empty update body, a value outside an allowed list (`ai_speed`, `anthropic_model`, `booking_provider`, `mode`, `type`), a non-weekday key in `availability`, a dotted field name on `bot-config`, or a malformed id in the path. |
| `403` | The account is not allowed to use a setting you sent, you are at your plan's Agent limit, or a feature this endpoint needs (media library, follow-ups, custom functions for MCP servers) is off. A change that exceeds the configuration size your plan allows is refused with `400`. |
| `404` | The Agent, tag rule, media item or MCP server was not found — either it does not exist or it belongs to another account. |
| `409` | Something is already in flight or in the way: an optimisation or tag generation is running, the Agent is still attached to a broadcast, Entry Point or campaign, or `cold_only` was asked for without an outgoing campaign. |

The shared codes every endpoint can return — `401`, `403` (your plan does not include API access), `429` (rate limit) and `500` — are listed with retry guidance in [Errors & Pagination](errors-and-pagination.md).

> **A note on the explorer.** The `/agents` endpoints are in the published OpenAPI specification, so you can browse their exact fields and run live requests in the [API Reference](reference.md). The account-level `/mcp-servers` endpoints are in the specification too, so you can explore them there as well.


---

## Related

- [AI Agents](../ai-agents/ai-agents.md) — what an Agent is, in plain language.
- [Entry Points](../ai-agents/entry-points.md) — how conversations get routed to an Agent.
- [FAQs API](faqs.md) — build and link the knowledge your Agent answers from.
- [Channels API](channels.md) — connect the channels an Agent answers on.
- [Connect MCP Servers to Your Bot](../ai-automation/mcp-servers.md) · [Custom Functions](../ai-automation/custom-functions.md)
- [API Reference](reference.md) — the full interactive endpoint explorer.
