API reference / Voice and AI
AI agents
An AI agent is a voice assistant that talks to callers. The same agent can answer a phone line (AI receptionist), place AI calls (Send an AI call), and run in campaigns. It answers from your knowledge bases and speaks with any of your voices.
API keys work on every route. An OAuth token issued for a user also needs that user's role to allow managing AI agents (see Role denials).
Routes
| Method | Route | Scope | What it does |
|---|---|---|---|
GET |
/agents/public/templates |
agents:read |
List templates |
POST |
/agents/public/agents/from-template |
agents:write |
Create an agent from a template |
GET |
/agents/public/agents |
agents:read |
List your agents |
POST |
/agents/public/agents |
agents:write |
Create an agent from scratch |
GET |
/agents/public/agents/{agent_id} |
agents:read |
Get an agent |
POST |
/agents/public/agents/{agent_id} |
agents:write |
Update an agent |
POST |
/agents/public/agents/{agent_id}/publish |
agents:write |
Publish an agent |
DELETE |
/agents/public/agents/{agent_id} |
agents:write |
Delete an agent |
Build order
An AI receptionist is an agent answering one of your phone lines. Each step needs an id from the one before:
- Knowledge base. Create one and add your hours, services and FAQs as
documents. Wait until each document's
ingest_statusisready, then test it with a query. See Knowledge bases. - Voice. Pick a
voice_idfromGET /voice/public/voices, or clone your own. - Agent. Create it from a template, with the voice and
think.knowledgepointing at your knowledge base. - Publish. Calls use the last published version, never the working copy.
- Phone line. Create a line that sends calls to the agent and assign a number to it. See AI receptionist.
- Test. Call the number, then watch for the call results.
For outbound calls, skip the phone line and pass the published agent's
agent_id to POST /ai-broadcast (see
Send an AI call).
List templates
Templates give you a working prompt, tools and defaults to start from.
| Field | Type | Required | Description |
|---|---|---|---|
direction |
string | No | inbound or outbound. |
category |
string | No | For example scheduling, sales or support. |
curl "https://api-v2.dropcowboy.com/agents/public/templates?direction=inbound" \
-H "x-key: $DC_KEY" -H "x-secret: $DC_SECRET"
{
"data": {
"templates": [
{
"template_id": "inbound-appointment-setting",
"name": "Appointment setting",
"description": "Book callback appointments and capture preferred date, time, and contact details.",
"direction": "inbound",
"category": "scheduling"
}
]
},
"meta": { "request_id": "c2a6e8f4-3b9d-4c1e-8a7f-5d3b1e9c6a24" }
}
Start from a template
| Field | Type | Required | Description |
|---|---|---|---|
template_id |
string | Yes | From the template list. |
name |
string | Yes | The agent's name. |
description |
string | No | A note for your team. |
active |
boolean | No | Whether the agent takes calls. |
curl -X POST https://api-v2.dropcowboy.com/agents/public/agents/from-template \
-H "x-key: $DC_KEY" -H "x-secret: $DC_SECRET" \
-H "content-type: application/json" \
-d '{
"template_id": "inbound-appointment-setting",
"name": "Front desk",
"active": true
}'
The route answers 201 with the agent. The template's
prompt, tools and defaults are copied into it, so later changes to the
template don't affect it. An unknown template_id answers 404.
List your agents
| Field | Type | Required | Description |
|---|---|---|---|
search_term |
string | No | Matches the agent name. |
limit |
integer | No | Page size. Default 50. |
curl "https://api-v2.dropcowboy.com/agents/public/agents?search_term=front" \
-H "x-key: $DC_KEY" -H "x-secret: $DC_SECRET"
The response carries data.agents, an array of agents, and
data.total.
Create an agent from scratch
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | The agent's name. |
description |
string | No | A note for your team. |
voice_id |
string | No | From GET /voice/public/voices, including your clones. |
language |
string | No | For example en. |
mode |
string | No | Stored as defaults.mode. |
think |
object | No | Prompt, tools and knowledge. See The think object. |
routing_targets |
array | No | Where the agent may transfer calls. |
contact_tools |
object | No | Which contact fields the agent may read and write. |
identity |
object | No | Caller identity checks. |
defaults |
object | No | Behaviour defaults. |
active |
boolean | No | Default true. |
curl -X POST https://api-v2.dropcowboy.com/agents/public/agents \
-H "x-key: $DC_KEY" -H "x-secret: $DC_SECRET" \
-H "content-type: application/json" \
-d '{
"name": "Front desk",
"voice_id": "7d2a9e4b-1c6f-4b3a-8e5d-2f9c7a1b4e63",
"language": "en",
"think": {
"prompt_spec": {
"role": "Receptionist for Example Dental",
"tone": "Warm and brief",
"objectives": ["Answer questions about hours and location", "Book cleanings"],
"intake_fields": ["first_name", "email"]
}
}
}'
The route answers 201 with the agent, including its
agent_id.
The think object
think holds how the agent reasons and what it can do.
| Key | Purpose |
|---|---|
prompt_spec |
A structured prompt: role, tone, objectives, intake_fields, prohibitions, business_context, additional_instructions. Drop Cowboy® compiles it into the system prompt, including guidance for the agent's tools and knowledge bases. |
llm_instructions |
The system prompt itself. It's generated for you when prompt_spec is set, so write one or the other. Placeholders such as {{contact.first_name}} are filled on each call. |
llm_functions |
HTTP tools the agent can call during a conversation: name, description, url, method, headers, and a JSON Schema in parameters. |
mcp_servers |
MCP servers the agent can use: name, transport (streamable-http or sse), url, headers. |
call_tools |
Built-in call controls such as hangup and send_dtmf. |
knowledge |
Knowledge bases to search. See Attach to an agent. |
Get an agent
curl https://api-v2.dropcowboy.com/agents/public/agents/4c8e2a6f-3b9d-4e1a-9c7b-5d2f8a3e6b19 \
-H "x-key: $DC_KEY" -H "x-secret: $DC_SECRET"
{
"data": {
"agent_id": "4c8e2a6f-3b9d-4e1a-9c7b-5d2f8a3e6b19",
"team_id": "3f6c2a1e-8b4d-4c7a-9e2f-5a1b3c4d6e7f",
"name": "Front desk",
"description": null,
"active": true,
"is_draft": false,
"voice_id": "7d2a9e4b-1c6f-4b3a-8e5d-2f9c7a1b4e63",
"language": "en",
"think": { "prompt_spec": { "role": "Receptionist for Example Dental" } },
"defaults": { "mode": "receptionist" },
"published_version": 3,
"published_at": 1774041600000,
"created_at": 1773955200000
},
"meta": { "request_id": "c2a6e8f4-3b9d-4c1e-8a7f-5d3b1e9c6a24" }
}
published_version and published_at are null until the first publish.
Timestamps are epoch milliseconds. A draft (is_draft: true) can't take calls.
Update an agent
Send only the fields you want to change. The fields are those of Create an agent from scratch, plus these:
| Field | Type | Required | Description |
|---|---|---|---|
asr_enabled |
boolean | No | Turns the live call transcript on or off. Only JSON true turns it on; any other value, including the string "true", turns it off. Leave it out to keep the current setting. |
post_call_automation_flow_ids |
array | No | Automation flows to run after each call. |
Changes reach calls only after you publish. The one exception is
active: false, which stops the agent taking new calls immediately.
Updating think replaces the whole object. If you send only
{"think": {"knowledge": {...}}}, the agent loses its prompt and tools. Read
the agent first, change the keys you need, and send the complete think back:
const base = 'https://api-v2.dropcowboy.com/agents/public/agents/' + agentId;
const headers = {
'x-key': process.env.DC_KEY,
'x-secret': process.env.DC_SECRET,
'content-type': 'application/json'
};
const { data: agent } = await (await fetch(base, { headers })).json();
const think = Object.assign({}, agent.think, {
knowledge: {
mode: 'selected',
knowledge_base_ids: ['9d3f6a2c-4e8b-4b1d-a7c5-2f6e9b3d8a14'],
audience: 'public'
}
});
await fetch(base, { method: 'POST', headers, body: JSON.stringify({ think }) });
await fetch(base + '/publish', { method: 'POST', headers });
Publish an agent
Publishing freezes the working copy as a new version. Phone lines, AI calls and campaigns use the latest published version.
curl -X POST https://api-v2.dropcowboy.com/agents/public/agents/4c8e2a6f-3b9d-4e1a-9c7b-5d2f8a3e6b19/publish \
-H "x-key: $DC_KEY" -H "x-secret: $DC_SECRET"
The response is the agent with its new published_version and
published_at. Publishing checks the agent first, and answers 400 with the
reason in details.code (see Errors) when:
- a tool definition is invalid;
- the agent references a tool connection or automation flow your account no longer has;
- a selected knowledge base reads like instructions for your staff rather than answers for callers.
Fix the problem and publish again.
Delete an agent
curl -X DELETE https://api-v2.dropcowboy.com/agents/public/agents/4c8e2a6f-3b9d-4e1a-9c7b-5d2f8a3e6b19 \
-H "x-key: $DC_KEY" -H "x-secret: $DC_SECRET"
AI calls queued for the agent are cancelled. Point any phone line that uses it at another agent first.
Call results
Subscribe to these webhooks to learn how calls
went. ai_agent.call.started fires when a call connects to the agent. Then
exactly one of these fires:
| Event | When |
|---|---|
ai_agent.call.failed |
The call failed, for example nobody answered. |
ai_agent.outcome.* |
The agent recorded an outcome: confirm, opt_out, transfer, voicemail, incomplete, timeout or identity_failed. |
ai_agent.call.completed |
Any other answered call. |
An inbound call looks like this. The payload also carries team, call and
contact objects, left out here:
{
"event_id": "d1f3a8e2-7c4b-4f9a-9d22-9c1e2f3a4b5c",
"event": "ai_agent.call.completed",
"event_at": 1774041600000,
"data": {
"team_id": "3f6c2a1e-8b4d-4c7a-9e2f-5a1b3c4d6e7f",
"agent_id": "4c8e2a6f-3b9d-4e1a-9c7b-5d2f8a3e6b19",
"call_id": "4e8c2a6d-1f9b-4d3e-a5c7-2b6f8d4a9e31",
"contact_id": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d",
"ivr_id": "e2b6f9a3-5c1d-4e8b-a4f7-9c3e1b5d7a28",
"outcome": null,
"answered": true,
"duration": 184.32,
"direction": "inbound"
}
}
ivr_id is the line_id of the phone line that was called. duration is in
seconds. Use call_id with GET /phone/public/calls/{call_id} and
GET /phone/public/calls/{call_id}/recording for details and the recording
(see Call history).
An AI call you send has no direction, agent_id or call_id. Instead it
carries campaign_id, session_id, drop_id, phone_number and
campaign_session, plus ai_duration_sec, ai_cost, transcript_length and
function_calls_count. Its outcome can also be voicemail_delivered,
no_response or a failure reason such as no_answer. Each contact's
delivery result also arrives on contact.rvm.status with
campaign_type: ai_broadcast.
Errors
The errors specific to agents are below. For the error format, 402 and
everything else, see Responses, errors and limits.
When a rejection has a specific reason, it's in details.code:
| Status | details.code |
Cause | What to do |
|---|---|---|---|
400 |
invalid_agent_tools |
A tool definition is invalid. | Fix the tool in think.llm_functions or think.mcp_servers. |
400 |
invalid_handoff_targets |
A transfer target is invalid. | Fix routing_targets. |
400 |
unusable_knowledge_base |
A selected knowledge base reads like staff instructions. | Rewrite its documents as answers for callers, or deselect it. |
400 |
unresolved_agent_references |
A tool connection or automation flow no longer exists. | Remove the reference or recreate what it points at. |
400 |
unresolved_voice_flow, voice_compile_error |
The agent's voice script isn't published, or doesn't compile. | Open the agent in the dashboard, fix its voice script and publish it. |
409 |
stale_published_version |
Another publish finished first. | Read the agent and publish again. |
Branch on status and details.code, not on detail. A detail of 200
characters or more is replaced by a generic message such as Invalid request.
Agents that Drop Cowboy manages for your account aren't listed. Getting,
updating, publishing or deleting one answers 404, the same as an unknown
agent_id.
Role denials
API keys are never limited by role. A token issued for a signed-in user is
checked against that user's role. A denial is a 403 with type ending
forbidden and one of these detail values:
detail |
Cause |
|---|---|
You do not have permission to manage AI Agents. |
The role has no AI agent access. Applies to any agent route. |
Not authorized to perform this AI chat action |
The role may not create, change, delete or publish agents. |
Ask an account admin to change the user's role, or use an API key.
{
"type": "https://api-v2.dropcowboy.com/errors/forbidden",
"title": "Forbidden",
"status": 403,
"detail": "You do not have permission to manage AI Agents.",
"instance": "/agents/public/agents",
"request_id": "8a2c4e6f-0b1d-4f3a-9c5e-7d9b1f3a5c84"
}
If the role can't be looked up, the answer is 500. Retry.