> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://developer.jelliu.co/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://developer.jelliu.co/_mcp/server.

# Agents

An **agent** is the AI persona that places and answers calls, replies on WhatsApp and email, and chats on your website. It carries everything that decides how a conversation goes: its objective (category), voice and language, the prompt you write, the first thing it says, the channels it may answer on, the post-call analysis it runs, and the tools and conversation graph it can use. Campaigns, phone numbers, widgets and inboxes all point at an agent.

Every agent belongs to one workspace. The API reads and writes the same agents you see under **Agents** in the dashboard.

## How it works

Creating an agent is a two-phase operation. Jelliu validates and screens your request, stores the agent and answers right away; the voice runtime for the agent is then provisioned in the background, which typically takes 30 to 45 seconds. Until that finishes, endpoints that need the runtime answer `409 AGENT_PROVISIONING`.

```mermaid
stateDiagram-v2
    [*] --> Provisioning: POST agents returns 201
    Provisioning --> Ready: background sync completes, about 30-45 s
    Provisioning --> Provisioning: sync retried, up to 3 attempts
    Ready --> Paused: POST pause
    Paused --> Ready: DELETE pause
    Provisioning --> Deleted: DELETE agent
    Ready --> Deleted: DELETE agent
    Paused --> Deleted: DELETE agent
    Deleted --> [*]
```

What happens on `POST /api/agents`, in order:

1. The body is validated. The system prompt, first message and objection handlers are screened for fraud; a blocked prompt fails with `403 COMPLIANCE_BLOCKED` before anything is stored.
2. Plan gates run: the agent cap for your plan, cloned voices (Enterprise only), and the dial policy for `transferPhoneNumber`.
3. The agent is inserted, under a lock that enforces the plan's agent cap atomically. Version 1 of its history is recorded.
4. The response returns `201` with the stored agent. Provisioning of the voice runtime is queued with up to 3 attempts and exponential backoff.

The agent object does not expose a provisioning flag. To know whether an agent is ready, call an endpoint that needs the runtime, such as `GET /api/agents/{agentId}/signed-url`: it answers `409 AGENT_PROVISIONING` with `Retry-After: 5` until provisioning completes. Adding tools and saving a workflow behave the same way.

### How the prompt is assembled

The agent's effective instructions are layered. Jelliu supplies a universal base for the agent's **category**, an optional per-objective playbook comes from the linked **template** (`templateId`), and your `systemPrompt` is the part you own. When you change the category, template or prompt, the agent is re-synced to its runtime.

* **`category`** is the agent's objective. It selects the base identity and the default post-call analysis. If you omit it on create, the linked template's category applies.
* **Runtime tuning** (`turnTimeout`, `turnEagerness`, `ttsSpeed`, `ttsStability`, `maxCallDurationSeconds`) is seeded from the category when you do not send a value, so each objective starts with pacing suited to it.
* **Analysis.** If you do not send `analysis`, a default set of success criteria and data-collection fields is generated for the category. Changing the category later regenerates that default analysis, unless the same request also sends `analysis`.

### Categories

| Value                 | Use it for                                                                         |
| --------------------- | ---------------------------------------------------------------------------------- |
| `sales`               | Selling and closing.                                                               |
| `support`             | Customer support.                                                                  |
| `scheduling`          | Booking and managing appointments.                                                 |
| `surveys`             | Surveys and feedback.                                                              |
| `collections`         | Payment collection.                                                                |
| `retention`           | Keeping customers who want to leave.                                               |
| `notifications`       | Informational outreach.                                                            |
| `interview`           | Candidate screening that ends in a hiring recommendation.                          |
| `language_assessment` | Structured spoken evaluations that end in a score (language level, skills checks). |
| `personal`            | A personal assistant that works for the person it talks to.                        |
| `general`             | Anything else.                                                                     |

### Channels

`channels` lists where the agent may answer: `voice`, `whatsapp`, `email`, `webchat`, `instagram` and `messenger`. An agent created without `channels` serves **all** of them.

`channels` is a full replacement, not a delta. Sending `["voice", "whatsapp"]` to an agent that serves every channel takes it off `email`, `webchat`, `instagram` and `messenger`. Always send the complete set you want.

## Object

`GET /api/agents` and `GET /api/agents/{agentId}` return agents with the fields below. Timestamps are ISO 8601 strings. Keys are `snake_case`, except for the three computed flags noted in the table.

| Field                                           | Type          | Nullable | Description                                                                                                                                                      |
| ----------------------------------------------- | ------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                                            | string (uuid) | No       | Agent ID.                                                                                                                                                        |
| `tenant_id`                                     | string (uuid) | No       | Workspace that owns the agent.                                                                                                                                   |
| `name`                                          | string        | No       | Display name, 1 to 100 characters.                                                                                                                               |
| `category`                                      | string        | Yes      | The agent's objective. See [Categories](#categories). `null` on legacy agents, which fall back to the template's category.                                       |
| `objective`                                     | string        | Yes      | Same value as `category`, kept for dashboard compatibility.                                                                                                      |
| `template_id`                                   | string (uuid) | Yes      | Linked template that contributes the objective playbook.                                                                                                         |
| `voice_id`                                      | string        | No       | Voice used on calls. List voices with `GET /api/voices`.                                                                                                         |
| `language`                                      | string        | No       | One of `es`, `es-CO`, `es-MX`, `es-AR`, `es-neutral`, `en`, `en-US`, `pt`, `pt-BR`.                                                                              |
| `channels`                                      | string\[]     | No       | Channels the agent may answer on.                                                                                                                                |
| `system_prompt`                                 | string        | No       | Your part of the instructions. Empty string means the generic role.                                                                                              |
| `first_message`                                 | string        | No       | What the agent says when a call connects.                                                                                                                        |
| `dynamic_variables`                             | object        | No       | Default values for variables used in the prompt, as string pairs.                                                                                                |
| `objection_handlers`                            | object        | No       | Map of objection to suggested answer.                                                                                                                            |
| `escalation_rules`                              | array         | No       | Items of `trigger`, `threshold`, `action`.                                                                                                                       |
| `transfer_rules`                                | array         | No       | In-call handoffs to other agents: `targetAgentId`, `condition`, `transferMessage`.                                                                               |
| `transfer_phone_number`                         | string        | Yes      | E.164 number the agent can transfer a live call to.                                                                                                              |
| `max_call_duration_seconds`                     | integer       | No       | Hard cap on call length, 30 to 3600. The category also acts as a ceiling.                                                                                        |
| `response_delay_seconds`                        | integer       | No       | Fixed pause before replying on text channels, 0 to 86400. `0` uses human pacing instead.                                                                         |
| `human_pacing_enabled`                          | boolean       | No       | Text channels reply on a growing delay (10 s, 1 min, 1 min 30 s, capped at 2 min) instead of instantly.                                                          |
| `enable_voicemail_detection`                    | boolean       | No       | Detect voicemail on outbound calls.                                                                                                                              |
| `disclose_ai`                                   | boolean       | Yes      | Whether the agent opens by saying it is an AI. `null` inherits the country policy.                                                                               |
| `department`                                    | string        | Yes      | Routing label, up to 100 characters.                                                                                                                             |
| `skills`                                        | string\[]     | Yes      | Routing skills, up to 50.                                                                                                                                        |
| `expressive_mode`                               | boolean       | No       | Expressive voice delivery.                                                                                                                                       |
| `tool_call_sound_enabled`                       | boolean       | No       | Play a sound while a tool runs.                                                                                                                                  |
| `text_normalisation_type`                       | string        | No       | `system_prompt` or `elevenlabs`.                                                                                                                                 |
| `turn_timeout`                                  | number        | No       | Seconds of silence before the agent takes the turn on voice.                                                                                                     |
| `turn_eagerness`                                | string        | No       | `patient`, `normal` or `eager` (legacy rows may carry `low`, `medium`, `high`).                                                                                  |
| `tts_speed`                                     | number        | No       | Speech speed, 0.5 to 2.0.                                                                                                                                        |
| `tts_stability`                                 | number        | No       | Voice stability, 0 to 1.                                                                                                                                         |
| `tts_similarity_boost`                          | number        | No       | Voice similarity, 0 to 1.                                                                                                                                        |
| `analysis`                                      | object        | Yes      | Post-call analysis: `successEvaluation` (criteria with `id`, `description`) and `dataCollection` (fields with `id`, `dataType`, `description`, optional `enum`). |
| `safety`                                        | object        | Yes      | `blockedInputTopics` and `blockedOutputTopics`.                                                                                                                  |
| `privacy`                                       | object        | Yes      | `redactPiiAudio`, `deleteAudioAfterProcessing`, `retentionDays`.                                                                                                 |
| `allowed_toolkits`                              | string\[]     | Yes      | Connected apps this agent may use. `null` means no restriction; `[]` means none.                                                                                 |
| `disabled_tools`                                | object        | Yes      | Per-app list of tool slugs switched off for this agent.                                                                                                          |
| `toolkit_access`                                | object        | Yes      | Per-app access level, `read` or `read_write`. Apps not listed default to `read_write`.                                                                           |
| `toolkitAccessEnforced`                         | boolean       | No       | Computed. `true` when `toolkit_access` is enforced.                                                                                                              |
| `mcp_enabled`                                   | boolean       | No       | Whether MCP servers may be attached to this agent.                                                                                                               |
| `mcpEnabled`                                    | boolean       | No       | Computed. Same value as `mcp_enabled`.                                                                                                                           |
| `tool_surface_status`                           | string        | No       | Whether tool changes have reached the runtime: `syncing`, `synced`, `error` or `pending_provisioning`.                                                           |
| `tool_surface_error`                            | string        | Yes      | Reason for the last tool sync error.                                                                                                                             |
| `tool_surface_updated_at`                       | string        | Yes      | When the tool sync status last changed.                                                                                                                          |
| `paused_at`                                     | string        | Yes      | When the agent was paused. `null` when running.                                                                                                                  |
| `total_calls`, `calls_today`, `conversion_rate` | number        | No       | Placeholders, currently always `0`. Use the analytics endpoints for real figures.                                                                                |
| `avg_sentiment`                                 | number        | Yes      | Placeholder, currently always `null`.                                                                                                                            |
| `created_at`                                    | string        | No       | Creation time.                                                                                                                                                   |
| `updated_at`                                    | string        | No       | Last update time.                                                                                                                                                |

`GET /api/agents/{agentId}` returns every stored column, so it also includes, among others:

| Field           | Type    | Nullable | Description                                                                                 |
| --------------- | ------- | -------- | ------------------------------------------------------------------------------------------- |
| `high_risk`     | boolean | No       | Changes to this agent's behavior need approval when the workspace has the approval gate on. |
| `paused_by`     | string  | Yes      | Who paused the agent. API keys appear as `apikey:` followed by the key ID.                  |
| `paused_reason` | string  | Yes      | The reason given when pausing.                                                              |
| `workflow`      | object  | Yes      | The conversation graph, or `null` when the agent runs from its prompt alone.                |
| `action_limits` | object  | Yes      | Tool-call budgets set under controls.                                                       |

Treat every field as optional and ignore fields you do not recognize. The detail response can carry internal columns that are not part of the contract.

## Common tasks

### Create an agent

#### Pick a voice

List the voice catalog and copy the `id` of a voice with `accessible: true`.

```bash
curl -sS "https://api.jelliu.co/api/voices" \
  -H "Authorization: Bearer $JELLIU_API_KEY"
```

The response is `{ "data": { "voices": [ ... ] } }`. Each voice has `id`, `name`, `previewUrl`, `category`, `gender`, `accent`, `age`, `language`, `useCase`, `description`, `accessible` and `inaccessibleReason`. Voices with `category: "cloned"` require the Enterprise plan.

#### Create the agent

Only `name`, `voiceId` and `language` are required.

**`curl`**

```bash title="curl"
curl -sS -X POST "https://api.jelliu.co/api/agents" \
  -H "Authorization: Bearer $JELLIU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Valentina",
    "voiceId": "JBFqnCBsd6RMkjVDRZzb",
    "language": "es-CO",
    "category": "scheduling",
    "channels": ["voice", "whatsapp"],
    "systemPrompt": "Eres Valentina, asistente de la Clínica Dental Sonrisa en Medellín. Ayudas a pacientes a agendar, mover o cancelar citas de valoración.",
    "firstMessage": "Hola, le habla Valentina de la Clínica Dental Sonrisa. ¿En qué le puedo ayudar?",
    "maxCallDurationSeconds": 300,
    "transferPhoneNumber": "+573001234567"
  }'
```

**`Node.js`**

```javascript title="Node.js"
const res = await fetch('https://api.jelliu.co/api/agents', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.JELLIU_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'Valentina',
    voiceId: 'JBFqnCBsd6RMkjVDRZzb',
    language: 'es-CO',
    category: 'scheduling',
    channels: ['voice', 'whatsapp'],
    systemPrompt:
      'Eres Valentina, asistente de la Clínica Dental Sonrisa en Medellín. Ayudas a pacientes a agendar, mover o cancelar citas de valoración.',
    firstMessage: 'Hola, le habla Valentina de la Clínica Dental Sonrisa. ¿En qué le puedo ayudar?',
    maxCallDurationSeconds: 300,
    transferPhoneNumber: '+573001234567',
  }),
});
const body = await res.json();
if (!res.ok) throw new Error(`${res.status} ${body.error?.code}`);

console.log(body.data.id, body.data.channels);
```

**`Python`**

```python title="Python"
import os
import requests

payload = {
    "name": "Valentina",
    "voiceId": "JBFqnCBsd6RMkjVDRZzb",
    "language": "es-CO",
    "category": "scheduling",
    "channels": ["voice", "whatsapp"],
    "systemPrompt": (
        "Eres Valentina, asistente de la Clínica Dental Sonrisa en Medellín. "
        "Ayudas a pacientes a agendar, mover o cancelar citas de valoración."
    ),
    "firstMessage": "Hola, le habla Valentina de la Clínica Dental Sonrisa. ¿En qué le puedo ayudar?",
    "maxCallDurationSeconds": 300,
    "transferPhoneNumber": "+573001234567",
}

res = requests.post(
    "https://api.jelliu.co/api/agents",
    json=payload,
    headers={"Authorization": f"Bearer {os.environ['JELLIU_API_KEY']}"},
    timeout=30,
)
body = res.json()
if not res.ok:
    raise RuntimeError(f"{res.status_code} {body['error']['code']}")

print(body["data"]["id"], body["data"]["channels"])
```

The request takes a second or two because the prompt is screened synchronously. A successful request returns `201 Created`:

```json
{
  "data": {
    "id": "3f6c2a1e-8b4d-4c7a-9e21-5d0b7a6c9f13",
    "tenant_id": "b2d4f6a8-1c3e-4a5b-8d7f-9e0a1b2c3d4e",
    "name": "Valentina",
    "category": "scheduling",
    "objective": "scheduling",
    "template_id": null,
    "voice_id": "JBFqnCBsd6RMkjVDRZzb",
    "language": "es-CO",
    "channels": ["voice", "whatsapp"],
    "system_prompt": "Eres Valentina, asistente de la Clínica Dental Sonrisa en Medellín. Ayudas a pacientes a agendar, mover o cancelar citas de valoración.",
    "first_message": "Hola, le habla Valentina de la Clínica Dental Sonrisa. ¿En qué le puedo ayudar?",
    "dynamic_variables": {},
    "objection_handlers": {},
    "escalation_rules": [],
    "transfer_rules": [],
    "transfer_phone_number": "+573001234567",
    "max_call_duration_seconds": 300,
    "response_delay_seconds": 0,
    "human_pacing_enabled": true,
    "enable_voicemail_detection": true,
    "disclose_ai": null,
    "department": null,
    "skills": [],
    "expressive_mode": false,
    "tool_call_sound_enabled": false,
    "text_normalisation_type": "elevenlabs",
    "tts_similarity_boost": 0.75,
    "analysis": {
      "successEvaluation": [ { "id": "appointment_booked", "description": "..." } ],
      "dataCollection": [ { "id": "appointment_date", "dataType": "string", "description": "..." } ]
    },
    "safety": null,
    "privacy": { "redactPiiAudio": true, "deleteAudioAfterProcessing": true, "retentionDays": 90 },
    "allowed_toolkits": null,
    "mcpEnabled": true,
    "toolkitAccessEnforced": true,
    "paused_at": null,
    "total_calls": 0,
    "calls_today": 0,
    "conversion_rate": 0,
    "avg_sentiment": null,
    "created_at": "2026-09-15T14:02:11.482Z",
    "updated_at": "2026-09-15T14:02:11.482Z"
  }
}
```

The default analysis above is abbreviated; its criteria depend on the category. Tuning fields such as `turn_timeout`, `turn_eagerness`, `tts_speed` and `tts_stability` are also present, with the values seeded from the category.

#### Wait for provisioning

Poll a runtime endpoint until it stops answering `409 AGENT_PROVISIONING`, honoring `Retry-After`.

**`curl`**

```bash title="curl"
curl -sS -i "https://api.jelliu.co/api/agents/3f6c2a1e-8b4d-4c7a-9e21-5d0b7a6c9f13/signed-url" \
  -H "Authorization: Bearer $JELLIU_API_KEY"
```

**`Node.js`**

```javascript title="Node.js"
async function waitUntilProvisioned(agentId, attempts = 20) {
  for (let i = 0; i < attempts; i++) {
    const res = await fetch(`https://api.jelliu.co/api/agents/${agentId}/signed-url`, {
      headers: { Authorization: `Bearer ${process.env.JELLIU_API_KEY}` },
    });
    if (res.ok) return;
    const body = await res.json();
    if (res.status !== 409 || body.error?.code !== 'AGENT_PROVISIONING') {
      throw new Error(`${res.status} ${body.error?.code}`);
    }
    const wait = Number(res.headers.get('Retry-After') ?? 5) * 1000;
    await new Promise((resolve) => setTimeout(resolve, wait));
  }
  throw new Error('Agent still provisioning');
}
```

**`Python`**

```python title="Python"
import os
import time
import requests

def wait_until_provisioned(agent_id, attempts=20):
    headers = {"Authorization": f"Bearer {os.environ['JELLIU_API_KEY']}"}
    for _ in range(attempts):
        res = requests.get(
            f"https://api.jelliu.co/api/agents/{agent_id}/signed-url",
            headers=headers,
            timeout=30,
        )
        if res.ok:
            return
        code = res.json().get("error", {}).get("code")
        if res.status_code != 409 or code != "AGENT_PROVISIONING":
            raise RuntimeError(f"{res.status_code} {code}")
        time.sleep(int(res.headers.get("Retry-After", "5")))
    raise RuntimeError("Agent still provisioning")
```

When ready, the endpoint returns `{ "data": { "signed_url": "wss://...", "conversation_token": "..." } }`. These are short-lived credentials for a browser test session; `conversation_token` is omitted when it cannot be minted.

#### Create fields

**`name`** `string` — required

1 to 100 characters.

---

**`voiceId`** `string` — required

A voice `id` from `GET /api/voices`. Cloned voices require the Enterprise plan.

---

**`language`** `string` — required

`es`, `es-CO`, `es-MX`, `es-AR`, `es-neutral`, `en`, `en-US`, `pt` or `pt-BR`. A few human labels are also accepted and normalized, for example `Spanish (LATAM)` becomes `es-neutral`.

---

**`category`** `string`

The agent's objective. See [Categories](#categories). Omit it to inherit the template's category.

---

**`templateId`** `string (uuid)`

A template from `GET /api/agent-templates`: a system template or one your workspace owns. Any other ID fails with `400 VALIDATION_FAILED`.

---

**`channels`** `string[]`

At least one of `voice`, `whatsapp`, `email`, `webchat`, `instagram`, `messenger`. Defaults to all of them.

---

**`systemPrompt`** `string` — default: empty

Up to 8,000 characters. Must be empty (the agent introduces itself generically and never invents a product) or at least 10 characters. Put catalogs, price lists and long scripts in the [knowledge base](/resources/knowledge-base) instead.

---

**`firstMessage`** `string`

Up to 500 characters. When omitted or blank, Jelliu generates a greeting in the agent's language with the agent name and your company name, for example `Hola, le habla Valentina de Clínica Dental Sonrisa. ¿Cómo está?`.

---

**`dynamicVariables`** `object` — default: \{}

Up to 50 entries. Keys up to 100 characters, values up to 500.

---

**`objectionHandlers`** `object` — default: \{}

Up to 50 entries. Keys up to 100 characters, values up to 2,000. Each value is fraud-screened.

---

**`escalationRules`** `array` — default: \[]

Items of `trigger` (`negative_sentiment`, `explicit_request`, `repeated_objection`, `timeout`), `threshold` (number, 0 or more) and `action` (`transfer_to_human`, `end_call_politely`, `schedule_callback`).

---

**`transferRules`** `array` — default: \[]

Up to 10 items of `targetAgentId` (uuid of another agent in your workspace), `condition` (1 to 500 characters) and optional `transferMessage` (up to 500). Targets that are not provisioned yet are skipped.

---

**`transferPhoneNumber`** `string`

E.164 (`+` and 2 to 15 digits). Checked against your workspace's dial policy when saved; a destination the policy refuses fails with `403 COMPLIANCE_BLOCKED`.

---

**`maxCallDurationSeconds`** `number`

30 to 3600. Defaults to the category's value.

---

**`discloseAi`** `boolean | null`

`true` or `false` overrides the country policy for this agent. Omit or send `null` to inherit it.

---

**`enableVoicemailDetection`** `boolean` — default: true

Detect voicemail on outbound calls.

---

**`department`** `string`

Up to 100 characters.

---

**`skills`** `string[]` — default: \[]

Up to 50 items of 1 to 100 characters.

---

**`analysis`** `object`

`successEvaluation`: up to 30 criteria of `id` (1 to 100 characters) and `description` (5 to 1,000). `dataCollection`: up to 40 fields of `id`, `dataType` (`string`, `boolean`, `integer`, `number`), `description` (5 to 1,000) and optional `enum` (up to 50 unique strings, only for `string` fields). Criterion IDs starting with `qa_` are reserved. Omit to get the category default.

---

**`safety`** `object`

`blockedInputTopics` and `blockedOutputTopics`: up to 20 topics each, up to 200 characters.

---

**`privacy`** `object`

`redactPiiAudio` (default `true`), `deleteAudioAfterProcessing` (default `true`), `retentionDays` (0 to 365, default `90`).

---

**`turnTimeout`** `number`

Voice only. 1 to 10 seconds. Defaults to the category's value.

---

**`turnEagerness`** `string`

`patient`, `normal`, `eager` (or legacy `low`, `medium`, `high`). Defaults to the category's value.

---

**`ttsSpeed`** `number`

0.5 to 2.0. Defaults to the category's value.

---

**`ttsStability`** `number`

0 to 1. Defaults to the category's value.

---

**`ttsSimilarityBoost`** `number` — default: 0.75

0 to 1.

---

**`expressiveMode`** `boolean` — default: false

Expressive voice delivery.

---

**`toolCallSoundEnabled`** `boolean` — default: false

Play a sound while a tool runs.

---

**`textNormalisationType`** `string` — default: elevenlabs

`system_prompt` or `elevenlabs`.

---

#### Asynchronous creation

Add `?async=true` to `POST /api/agents` to run the whole creation, including fraud screening, in the background. The response is `202 Accepted`:

```json
{ "data": { "jobId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", "status": "pending" } }
```

Poll `GET /api/agents/jobs/{jobId}`. `data.status` moves through `pending`, `processing`, then `completed` (with the agent in `data.result`) or `failed` (with the reason in `data.error`). Job records expire after one hour. The body is still validated synchronously, so an invalid request fails with `400` before a job is created.

### List and search agents

`GET /api/agents` returns up to `limit` agents (1 to 200, default 50), newest first. Pass the `created_at` of the last agent as `cursor` for the next page, and `search` for a case-insensitive match on the name. See [Pagination](/pagination#agents).

**`curl`**

```bash title="curl"
curl -sS "https://api.jelliu.co/api/agents?limit=50&search=valentina" \
  -H "Authorization: Bearer $JELLIU_API_KEY"
```

**`Node.js`**

```javascript title="Node.js"
const url = new URL('https://api.jelliu.co/api/agents');
url.searchParams.set('limit', '50');
url.searchParams.set('search', 'valentina');

const res = await fetch(url, {
  headers: { Authorization: `Bearer ${process.env.JELLIU_API_KEY}` },
});
const { data } = await res.json();
for (const agent of data) console.log(agent.id, agent.name, agent.paused_at);
```

**`Python`**

```python title="Python"
import os
import requests

res = requests.get(
    "https://api.jelliu.co/api/agents",
    params={"limit": 50, "search": "valentina"},
    headers={"Authorization": f"Bearer {os.environ['JELLIU_API_KEY']}"},
    timeout=30,
)
for agent in res.json()["data"]:
    print(agent["id"], agent["name"], agent["paused_at"])
```

```json
{
  "data": [
    {
      "id": "3f6c2a1e-8b4d-4c7a-9e21-5d0b7a6c9f13",
      "name": "Valentina",
      "category": "scheduling",
      "objective": "scheduling",
      "language": "es-CO",
      "channels": ["voice", "whatsapp"],
      "paused_at": null,
      "tool_surface_status": "synced",
      "created_at": "2026-09-15T14:02:11.482Z",
      "updated_at": "2026-09-15T14:02:11.482Z"
    }
  ]
}
```

The list response above is abbreviated; each item carries the fields in [Object](#object) except the detail-only ones. Get one agent with `GET /api/agents/{agentId}`.

Agent lists and details are cached for up to 120 seconds. Creating, updating and deleting agents through the API clears the cache, but pausing does not. For the current pause state, read `GET /api/agents/{agentId}/pause`.

### Update an agent

`PATCH /api/agents/{agentId}` accepts any subset of the create fields, and at least one field must be present. It also accepts fields that exist only on update:

| Field                  | Type                | Description                                                                                             |
| ---------------------- | ------------------- | ------------------------------------------------------------------------------------------------------- |
| `responseDelaySeconds` | integer             | Fixed text-channel reply pause, 0 to 86400. Above `0` it wins over human pacing.                        |
| `humanPacingEnabled`   | boolean             | Turn human pacing on text channels on or off. Voice is unaffected.                                      |
| `allowedToolkits`      | string\[] or `null` | Connected apps the agent may use (up to 300 slugs). `null` removes the restriction; `[]` allows none.   |
| `disabledTools`        | object or `null`    | Per-app lists of tool slugs to switch off, for example `{ "hubspot": ["HUBSPOT_DELETE_DEAL"] }`.        |
| `toolkitAccess`        | object or `null`    | Per-app `read` or `read_write`. Merged key by key with what is stored; `null` clears every restriction. |
| `mcpEnabled`           | boolean             | `false` also detaches MCP servers already attached to this agent.                                       |
| `highRisk`             | boolean             | Mark the agent as high risk for the approval gate. See [Change approval](#change-approval).             |

Nullable fields on update: `templateId`, `category`, `transferPhoneNumber`, `discloseAi` and `department` accept `null` to clear them.

**`curl`**

```bash title="curl"
curl -sS -X PATCH "https://api.jelliu.co/api/agents/3f6c2a1e-8b4d-4c7a-9e21-5d0b7a6c9f13" \
  -H "Authorization: Bearer $JELLIU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "firstMessage": "Hola, gracias por comunicarse con la Clínica Dental Sonrisa. Le habla Valentina.",
    "ttsSpeed": 0.95,
    "analysis": {
      "dataCollection": [
        { "id": "treatment_interest", "dataType": "string", "description": "Tratamiento por el que pregunta el paciente", "enum": ["valoracion", "ortodoncia", "blanqueamiento", "otro"] }
      ]
    }
  }'
```

**`Node.js`**

```javascript title="Node.js"
const agentId = '3f6c2a1e-8b4d-4c7a-9e21-5d0b7a6c9f13';
const res = await fetch(`https://api.jelliu.co/api/agents/${agentId}`, {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.JELLIU_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    firstMessage: 'Hola, gracias por comunicarse con la Clínica Dental Sonrisa. Le habla Valentina.',
    ttsSpeed: 0.95,
    analysis: {
      dataCollection: [
        {
          id: 'treatment_interest',
          dataType: 'string',
          description: 'Tratamiento por el que pregunta el paciente',
          enum: ['valoracion', 'ortodoncia', 'blanqueamiento', 'otro'],
        },
      ],
    },
  }),
});
const body = await res.json();
if (res.status === 409 && body.error.code === 'AGENT_CHANGE_PENDING_APPROVAL') {
  console.log('Held for review:', body.error.metadata.changeRequestId);
} else if (!res.ok) {
  throw new Error(`${res.status} ${body.error?.code}`);
}
```

**`Python`**

```python title="Python"
import os
import requests

agent_id = "3f6c2a1e-8b4d-4c7a-9e21-5d0b7a6c9f13"
res = requests.patch(
    f"https://api.jelliu.co/api/agents/{agent_id}",
    json={
        "firstMessage": "Hola, gracias por comunicarse con la Clínica Dental Sonrisa. Le habla Valentina.",
        "ttsSpeed": 0.95,
        "analysis": {
            "dataCollection": [
                {
                    "id": "treatment_interest",
                    "dataType": "string",
                    "description": "Tratamiento por el que pregunta el paciente",
                    "enum": ["valoracion", "ortodoncia", "blanqueamiento", "otro"],
                }
            ]
        },
    },
    headers={"Authorization": f"Bearer {os.environ['JELLIU_API_KEY']}"},
    timeout=30,
)
body = res.json()
if res.status_code == 409 and body["error"]["code"] == "AGENT_CHANGE_PENDING_APPROVAL":
    print("Held for review:", body["error"]["metadata"]["changeRequestId"])
elif not res.ok:
    raise RuntimeError(f"{res.status_code} {body['error']['code']}")
```

The response is `200` with the updated agent in `data`.

How update merges values:

* **`analysis`, `safety`, `privacy`** are merged half by half. An absent key keeps what is stored, `null` clears that half, and a value replaces it. `null` for the whole block clears it. In the example above, the stored `successEvaluation` is kept.
* **`toolkitAccess`** is merged per app. **`channels`**, **`skills`**, **`escalationRules`**, **`transferRules`**, **`dynamicVariables`** and **`objectionHandlers`** are replaced whole.
* **`systemPrompt`** on update must be 10 to 8,000 characters; an empty string is treated as not sent.

Unknown fields are silently dropped. A misspelled key such as `system_prompt` in a `PATCH` body returns `200` and changes nothing. Use the camelCase names documented here.

### Estimate a prompt's cost

`POST /api/agents/prompt-cost` measures a draft against the base instructions for its category before you save it. The body is `systemPrompt` (up to 200,000 characters, so an over-limit draft can still be measured) and optional `category`.

```bash
curl -sS -X POST "https://api.jelliu.co/api/agents/prompt-cost" \
  -H "Authorization: Bearer $JELLIU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "category": "scheduling", "systemPrompt": "Eres Valentina, asistente de la Clínica Dental Sonrisa..." }'
```

```json
{
  "data": {
    "clientChars": 58,
    "totalChars": 15342,
    "totalTokens": 3836,
    "guidelineTokens": 2000,
    "overGuideline": true,
    "model": "gemini-3.8-flash",
    "usdPerMinute": 0.0071
  }
}
```

Tokens are estimated at 4 characters per token over the full assembled prompt, and `overGuideline` flags prompts above 2,000 tokens, where latency and cost start to rise. `usdPerMinute` is `null` when the price cannot be fetched.

### Pause and resume an agent

Pausing is the emergency stop. A paused agent accepts no **new** calls or conversation turns; attempts fail with `409 AGENT_PAUSED` (or `503 AGENT_PAUSED` when the pause state cannot be read, so the stop fails closed). Conversations already in progress continue to their natural end unless you also set `terminateActive`.

#### Pause

`reason` is required (10 to 500 characters) and is written to the audit log.

**`curl`**

```bash title="curl"
curl -sS -X POST "https://api.jelliu.co/api/agents/3f6c2a1e-8b4d-4c7a-9e21-5d0b7a6c9f13/pause" \
  -H "Authorization: Bearer $JELLIU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Quoting outdated prices after the catalog change", "terminateActive": true }'
```

**`Node.js`**

```javascript title="Node.js"
const res = await fetch(
  'https://api.jelliu.co/api/agents/3f6c2a1e-8b4d-4c7a-9e21-5d0b7a6c9f13/pause',
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.JELLIU_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      reason: 'Quoting outdated prices after the catalog change',
      terminateActive: true,
    }),
  },
);
const { data } = await res.json();
console.log(data.paused, data.termination);
```

**`Python`**

```python title="Python"
import os
import requests

res = requests.post(
    "https://api.jelliu.co/api/agents/3f6c2a1e-8b4d-4c7a-9e21-5d0b7a6c9f13/pause",
    json={
        "reason": "Quoting outdated prices after the catalog change",
        "terminateActive": True,
    },
    headers={"Authorization": f"Bearer {os.environ['JELLIU_API_KEY']}"},
    timeout=60,
)
data = res.json()["data"]
print(data["paused"], data["termination"])
```

```json
{
  "data": {
    "paused": true,
    "pausedAt": "2026-09-15T16:20:03.114Z",
    "pausedBy": "apikey:6d1f0c2b-4a9e-4f7b-8c3d-2e1a0b9c8d7f",
    "reason": "Quoting outdated prices after the catalog change",
    "termination": {
      "attempted": 3,
      "terminated": 2,
      "stillLiveWithoutCarrierLeg": 1,
      "failed": 0,
      "truncated": false,
      "elapsedMs": 812,
      "slowestCallMs": 431,
      "providerSupportsLiveTermination": false
    },
    "note": "No se aceptan llamadas ni turnos NUEVOS. Se colgaron 2 de 3 llamadas en curso, ..."
  }
}
```

`termination` is `null` unless `terminateActive` is `true`. Pausing an agent that is already paused keeps the original `pausedAt`, returns `alreadyPaused: true`, and still runs the termination sweep if you ask for it.

#### Resume

```bash
curl -sS -X DELETE "https://api.jelliu.co/api/agents/3f6c2a1e-8b4d-4c7a-9e21-5d0b7a6c9f13/pause" \
  -H "Authorization: Bearer $JELLIU_API_KEY"
```

Returns `{ "data": { "paused": false } }`, or `{ "data": { "paused": false, "alreadyRunning": true } }` if it was not paused. Both calls are idempotent.

`terminateActive` hangs up the telephony leg of each live call. Calls with no carrier leg of Jelliu's own (a SIP trunk, or a number registered directly with the voice provider) cannot be cut: they are counted in `stillLiveWithoutCarrierLeg` and keep running. If `truncated` is `true`, there were more live calls than one sweep handles; repeat the request.

`GET /api/agents/{agentId}/pause` returns `paused`, `pausedAt`, `pausedBy`, `reason` and `note` without changing anything.

### Delete an agent

`DELETE /api/agents/{agentId}` returns `204 No Content`. Deleting:

* **pauses every active campaign** that uses the agent, so they stop dialing on their next cycle;
* unassigns the agent from its phone numbers, so inbound calls to those numbers need a new agent;
* removes the agent from the voice runtime first, and aborts the delete if that fails, so no orphan is left behind;
* is a soft delete on Jelliu's side: the version history is kept.

Campaigns paused by the delete must be reassigned to another agent before they can resume.

### Version history

Every change to an agent's behavior is recorded as a numbered version: creation, updates, workflow edits, restores and deletion.

| Method | Path                                               | Returns                                                                            |
| ------ | -------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `GET`  | `/api/agents/{agentId}/versions?limit=50`          | Versions, newest first. `limit` 1 to 200, default 50. Without the `snapshot` body. |
| `GET`  | `/api/agents/{agentId}/versions/{version}`         | One version, including `snapshot`.                                                 |
| `POST` | `/api/agents/{agentId}/versions/{version}/restore` | The restored agent. Requires a `full` key.                                         |

A version has `id`, `agent_id`, `version`, `change_source` (`create`, `update`, `approved_update`, `workflow_put`, `workflow_delete`, `base_resync`, `tool_surface`, `restore`, `delete`), `changed_by`, `actor_type`, `summary`, `diff`, `snapshot_sha256`, `snapshot_pruned_at`, `restored_from_version` and `created_at`.

The version record is kept forever, but its `snapshot` is removed once the version is older than 400 days **and** is not among the agent's 50 most recent versions. Restoring such a version fails with `410` and code `VALIDATION_FAILED`. Restoring to a state identical to the current one is a no-op.

### Change approval

A workspace can require a second person to approve behavior changes to agents marked `highRisk`. It takes both switches: the workspace setting (off by default) and the agent's `highRisk` flag.

When both are on, a `PATCH` that touches a behavior field, a restore, or a workflow change is **not applied**. The API answers `409 AGENT_CHANGE_PENDING_APPROVAL` and returns the request ID:

```json
{
  "error": {
    "code": "AGENT_CHANGE_PENDING_APPROVAL",
    "message": "Este agente está marcado de riesgo alto: el cambio quedó pendiente de que otro usuario lo apruebe.",
    "metadata": {
      "changeRequestId": "c7e1a2b3-4d5f-4a6b-9c8d-7e6f5a4b3c2d",
      "fields": "firstMessage, systemPrompt"
    }
  }
}
```

Behavior fields are `systemPrompt`, `firstMessage`, `category`, `templateId`, `objectionHandlers`, `escalationRules`, `transferRules`, `transferPhoneNumber`, `enableVoicemailDetection`, `safety`, `privacy`, `analysis`, `allowedToolkits`, `disabledTools`, `toolkitAccess`, `mcpEnabled`, `channels`, `maxCallDurationSeconds` and `highRisk` itself. Renames, voice, language, pacing and TTS tuning pass straight through.

| Method | Path                                                              | Required                           |
| ------ | ----------------------------------------------------------------- | ---------------------------------- |
| `GET`  | `/api/agents/change-requests?status=pending&agentId=...&limit=50` | `read` key                         |
| `POST` | `/api/agents/change-requests/{id}/approve`                        | `full` key                         |
| `POST` | `/api/agents/change-requests/{id}/reject`                         | `full` key                         |
| `POST` | `/api/agents/change-requests/{id}/cancel`                         | `write` key, and only the proposer |

`status` is one of `pending`, `approved`, `rejected`, `applied`, `failed`, `cancelled`. `approve` and `reject` accept an optional `note` (up to 2,000 characters). An approved change is re-validated against the current schema and applied; if applying fails, the request ends as `failed` and must be proposed again.

The proposer cannot approve or reject their own request (`403 FORBIDDEN`). An API key's identity is the key itself, so a change proposed with one key must be reviewed by a person in the dashboard or with a different key.

### Tools, workflows and connected apps

These are managed under the agent and are covered in the [API reference](/api-reference):

* **Tools** (`/api/agents/{agentId}/tools`): webhook tools the agent can call during a conversation, with method, URL, parameters and authentication.
* **Workflow** (`GET`, `PUT`, `DELETE` `/api/agents/{agentId}/workflow`): an optional conversation graph of up to 12 nodes and 24 edges. Nodes are `subagent` steps or `end`; edges are `unconditional`, `llm` (a natural-language condition) or `expression` (a comparison over up to 6 state variables the agent scores as it goes). Without a workflow the agent runs from its prompt alone.
* **Knowledge** (`/api/agents/{agentId}/knowledge`): documents the agent retrieves from. See [Knowledge base](/resources/knowledge-base).
* **MCP servers** (`/api/agents/{agentId}/mcp-servers`): external tool servers attached to the agent, allowed only while `mcpEnabled` is `true`.
* **Controls** (`/api/agents/{agentId}/controls/limits`): tool-call budgets `perMinute`, `perHour`, `perDay` and `perConversation`. When exhausted, actions fail with `429 AGENT_BUDGET_EXCEEDED`. `maxSpendPerDayCents` is stored but not enforced yet, and the response says so with `spendEnforced: false`.

Tool and workflow changes need a provisioned agent and answer `409 AGENT_PROVISIONING` until then.

## Errors

| Code                            | Status | When                                                                                                                                                                                                                  |
| ------------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `VALIDATION_FAILED`             | 400    | The body, query or `agentId` is invalid; an update has no fields; `templateId` is not accessible; an analysis criterion ID starts with `qa_`. `details` carries `formErrors` and `fieldErrors` for create and update. |
| `VALIDATION_FAILED`             | 409    | A change request is no longer `pending`.                                                                                                                                                                              |
| `VALIDATION_FAILED`             | 410    | The version's snapshot expired and cannot be restored.                                                                                                                                                                |
| `FORBIDDEN`                     | 403    | The key's scope does not allow the operation, or a proposer tried to approve or reject their own change request.                                                                                                      |
| `BILLING_ERROR`                 | 403    | The plan's agent cap was reached (`metadata` has `limit`, `current`, `tier`), there is no active plan, or a cloned voice was used without Enterprise.                                                                 |
| `COMPLIANCE_BLOCKED`            | 403    | Fraud screening blocked the prompt, first message or an objection handler, or the dial policy refused `transferPhoneNumber`.                                                                                          |
| `AGENT_NOT_FOUND`               | 404    | The agent does not exist, was deleted, or belongs to another workspace.                                                                                                                                               |
| `NOT_FOUND`                     | 404    | The version, change request or async job does not exist.                                                                                                                                                              |
| `AGENT_PROVISIONING`            | 409    | The agent's runtime is still being set up. Retry after `Retry-After` (5 seconds).                                                                                                                                     |
| `AGENT_CHANGE_PENDING_APPROVAL` | 409    | The change was held for review and **not** applied.                                                                                                                                                                   |
| `AGENT_PAUSED`                  | 409    | A call or turn was attempted on a paused agent. `503` when the pause state could not be read.                                                                                                                         |
| `RATE_LIMIT_EXCEEDED`           | 429    | Too many agent mutations. See [Limits](#limits).                                                                                                                                                                      |
| `TOOL_SYNC_FAILED`              | 502    | A restore could not be applied to the runtime. The agent was left unchanged.                                                                                                                                          |

See [Errors](/errors) for the envelope and retry guidance.

## Limits

**Rate limits.** Agent routes are under the general API limit. Mutations (`POST /api/agents`, `PATCH`, `DELETE`, `prompt-cost`, restoring a version, approving a change request, and tool and workflow writes) also share the 10-per-minute configuration budget with campaigns and webhooks. See [Rate limits](/rate-limits).

**Plan limits.** Agents per workspace, including agent add-ons you buy on top:

| Plan       | Agents |
| ---------- | ------ |
| Starter    | 2      |
| Growth     | 5      |
| Business   | 12     |
| Enterprise | 9999   |

A workspace without an active plan cannot create agents. Cloned voices are available on Enterprise only.

**Scopes.**

| Operation                                                                        | Minimum key scope |
| -------------------------------------------------------------------------------- | ----------------- |
| List, get, signed URL, pause state, versions, change requests, voices, templates | `read`            |
| Create, update, prompt cost, pause, resume, cancel own change request            | `write`           |
| Delete, restore a version, approve or reject a change request                    | `full`            |

See [Authentication](/authentication) for how scopes map to roles.

## Webhooks

| Event                            | Sent    | Relevance                                                             |
| -------------------------------- | ------- | --------------------------------------------------------------------- |
| `agent.action_recorded`          | Yes     | One event each time an agent invokes a tool.                          |
| `call.completed`, `call.failed`  | Yes     | Carry `agentId`; filter a webhook on `agentIds` to follow one agent.  |
| `audit.log_recorded`             | Yes     | Pauses and resumes are audited as `AGENT_PAUSED` and `AGENT_RESUMED`. |
| `agent.created`, `agent.updated` | Not yet | Accepted in subscriptions but not delivered.                          |

See [Webhooks](/webhooks) for payloads and signature verification.

## Related

#### [Calls](/resources/calls)

Place outbound calls with an agent and follow their lifecycle.

#### [Campaigns](/resources/campaigns)

Run an agent over a list of contacts on a schedule.

#### [Knowledge base](/resources/knowledge-base)

Give an agent documents to answer from.

#### [Phone numbers](/resources/phone-numbers)

Assign numbers so an agent answers inbound calls.

#### [Conversations](/resources/conversations)

Read what the agent said across every channel.

#### [API reference](/api-reference)

Every agent endpoint, parameter and response.