Create Goal Bot
curl --request POST \
--url https://app.tuco.ai/api/goal-bots \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"name": "<string>",
"prompt": "<string>",
"includeTags": [
"<string>"
],
"excludeTags": [
"<string>"
],
"mode": "<string>",
"status": "<string>",
"connector": "<string>",
"kbRetrieval": "<string>",
"tone": "<string>",
"canAsk": true,
"typingIndicator": true,
"maxTurns": 123,
"onDoneTag": "<string>",
"writeTagToCrm": true,
"allowedToolIds": [
"<string>"
],
"collectFields": [
"<string>"
]
}
'import requests
url = "https://app.tuco.ai/api/goal-bots"
payload = {
"name": "<string>",
"prompt": "<string>",
"includeTags": ["<string>"],
"excludeTags": ["<string>"],
"mode": "<string>",
"status": "<string>",
"connector": "<string>",
"kbRetrieval": "<string>",
"tone": "<string>",
"canAsk": True,
"typingIndicator": True,
"maxTurns": 123,
"onDoneTag": "<string>",
"writeTagToCrm": True,
"allowedToolIds": ["<string>"],
"collectFields": ["<string>"]
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
name: '<string>',
prompt: '<string>',
includeTags: ['<string>'],
excludeTags: ['<string>'],
mode: '<string>',
status: '<string>',
connector: '<string>',
kbRetrieval: '<string>',
tone: '<string>',
canAsk: true,
typingIndicator: true,
maxTurns: 123,
onDoneTag: '<string>',
writeTagToCrm: true,
allowedToolIds: ['<string>'],
collectFields: ['<string>']
})
};
fetch('https://app.tuco.ai/api/goal-bots', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://app.tuco.ai/api/goal-bots",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'name' => '<string>',
'prompt' => '<string>',
'includeTags' => [
'<string>'
],
'excludeTags' => [
'<string>'
],
'mode' => '<string>',
'status' => '<string>',
'connector' => '<string>',
'kbRetrieval' => '<string>',
'tone' => '<string>',
'canAsk' => true,
'typingIndicator' => true,
'maxTurns' => 123,
'onDoneTag' => '<string>',
'writeTagToCrm' => true,
'allowedToolIds' => [
'<string>'
],
'collectFields' => [
'<string>'
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://app.tuco.ai/api/goal-bots"
payload := strings.NewReader("{\n \"name\": \"<string>\",\n \"prompt\": \"<string>\",\n \"includeTags\": [\n \"<string>\"\n ],\n \"excludeTags\": [\n \"<string>\"\n ],\n \"mode\": \"<string>\",\n \"status\": \"<string>\",\n \"connector\": \"<string>\",\n \"kbRetrieval\": \"<string>\",\n \"tone\": \"<string>\",\n \"canAsk\": true,\n \"typingIndicator\": true,\n \"maxTurns\": 123,\n \"onDoneTag\": \"<string>\",\n \"writeTagToCrm\": true,\n \"allowedToolIds\": [\n \"<string>\"\n ],\n \"collectFields\": [\n \"<string>\"\n ]\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://app.tuco.ai/api/goal-bots")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"<string>\",\n \"prompt\": \"<string>\",\n \"includeTags\": [\n \"<string>\"\n ],\n \"excludeTags\": [\n \"<string>\"\n ],\n \"mode\": \"<string>\",\n \"status\": \"<string>\",\n \"connector\": \"<string>\",\n \"kbRetrieval\": \"<string>\",\n \"tone\": \"<string>\",\n \"canAsk\": true,\n \"typingIndicator\": true,\n \"maxTurns\": 123,\n \"onDoneTag\": \"<string>\",\n \"writeTagToCrm\": true,\n \"allowedToolIds\": [\n \"<string>\"\n ],\n \"collectFields\": [\n \"<string>\"\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.tuco.ai/api/goal-bots")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"name\": \"<string>\",\n \"prompt\": \"<string>\",\n \"includeTags\": [\n \"<string>\"\n ],\n \"excludeTags\": [\n \"<string>\"\n ],\n \"mode\": \"<string>\",\n \"status\": \"<string>\",\n \"connector\": \"<string>\",\n \"kbRetrieval\": \"<string>\",\n \"tone\": \"<string>\",\n \"canAsk\": true,\n \"typingIndicator\": true,\n \"maxTurns\": 123,\n \"onDoneTag\": \"<string>\",\n \"writeTagToCrm\": true,\n \"allowedToolIds\": [\n \"<string>\"\n ],\n \"collectFields\": [\n \"<string>\"\n ]\n}"
response = http.request(request)
puts response.read_bodyGoal Bots
Create Goal Bot
Create a goal bot: one prompt, the tools it may use, and the tags that fire it. No stages. Pass status paused to create it dormant.
POST
/
api
/
goal-bots
Create Goal Bot
curl --request POST \
--url https://app.tuco.ai/api/goal-bots \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"name": "<string>",
"prompt": "<string>",
"includeTags": [
"<string>"
],
"excludeTags": [
"<string>"
],
"mode": "<string>",
"status": "<string>",
"connector": "<string>",
"kbRetrieval": "<string>",
"tone": "<string>",
"canAsk": true,
"typingIndicator": true,
"maxTurns": 123,
"onDoneTag": "<string>",
"writeTagToCrm": true,
"allowedToolIds": [
"<string>"
],
"collectFields": [
"<string>"
]
}
'import requests
url = "https://app.tuco.ai/api/goal-bots"
payload = {
"name": "<string>",
"prompt": "<string>",
"includeTags": ["<string>"],
"excludeTags": ["<string>"],
"mode": "<string>",
"status": "<string>",
"connector": "<string>",
"kbRetrieval": "<string>",
"tone": "<string>",
"canAsk": True,
"typingIndicator": True,
"maxTurns": 123,
"onDoneTag": "<string>",
"writeTagToCrm": True,
"allowedToolIds": ["<string>"],
"collectFields": ["<string>"]
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
name: '<string>',
prompt: '<string>',
includeTags: ['<string>'],
excludeTags: ['<string>'],
mode: '<string>',
status: '<string>',
connector: '<string>',
kbRetrieval: '<string>',
tone: '<string>',
canAsk: true,
typingIndicator: true,
maxTurns: 123,
onDoneTag: '<string>',
writeTagToCrm: true,
allowedToolIds: ['<string>'],
collectFields: ['<string>']
})
};
fetch('https://app.tuco.ai/api/goal-bots', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://app.tuco.ai/api/goal-bots",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'name' => '<string>',
'prompt' => '<string>',
'includeTags' => [
'<string>'
],
'excludeTags' => [
'<string>'
],
'mode' => '<string>',
'status' => '<string>',
'connector' => '<string>',
'kbRetrieval' => '<string>',
'tone' => '<string>',
'canAsk' => true,
'typingIndicator' => true,
'maxTurns' => 123,
'onDoneTag' => '<string>',
'writeTagToCrm' => true,
'allowedToolIds' => [
'<string>'
],
'collectFields' => [
'<string>'
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://app.tuco.ai/api/goal-bots"
payload := strings.NewReader("{\n \"name\": \"<string>\",\n \"prompt\": \"<string>\",\n \"includeTags\": [\n \"<string>\"\n ],\n \"excludeTags\": [\n \"<string>\"\n ],\n \"mode\": \"<string>\",\n \"status\": \"<string>\",\n \"connector\": \"<string>\",\n \"kbRetrieval\": \"<string>\",\n \"tone\": \"<string>\",\n \"canAsk\": true,\n \"typingIndicator\": true,\n \"maxTurns\": 123,\n \"onDoneTag\": \"<string>\",\n \"writeTagToCrm\": true,\n \"allowedToolIds\": [\n \"<string>\"\n ],\n \"collectFields\": [\n \"<string>\"\n ]\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://app.tuco.ai/api/goal-bots")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"<string>\",\n \"prompt\": \"<string>\",\n \"includeTags\": [\n \"<string>\"\n ],\n \"excludeTags\": [\n \"<string>\"\n ],\n \"mode\": \"<string>\",\n \"status\": \"<string>\",\n \"connector\": \"<string>\",\n \"kbRetrieval\": \"<string>\",\n \"tone\": \"<string>\",\n \"canAsk\": true,\n \"typingIndicator\": true,\n \"maxTurns\": 123,\n \"onDoneTag\": \"<string>\",\n \"writeTagToCrm\": true,\n \"allowedToolIds\": [\n \"<string>\"\n ],\n \"collectFields\": [\n \"<string>\"\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.tuco.ai/api/goal-bots")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"name\": \"<string>\",\n \"prompt\": \"<string>\",\n \"includeTags\": [\n \"<string>\"\n ],\n \"excludeTags\": [\n \"<string>\"\n ],\n \"mode\": \"<string>\",\n \"status\": \"<string>\",\n \"connector\": \"<string>\",\n \"kbRetrieval\": \"<string>\",\n \"tone\": \"<string>\",\n \"canAsk\": true,\n \"typingIndicator\": true,\n \"maxTurns\": 123,\n \"onDoneTag\": \"<string>\",\n \"writeTagToCrm\": true,\n \"allowedToolIds\": [\n \"<string>\"\n ],\n \"collectFields\": [\n \"<string>\"\n ]\n}"
response = http.request(request)
puts response.read_bodyGoal bots and task agents are the same object. The two engines merged;
/api/goal-bots
and /api/tasks are aliases over one collection and return identical bodies. Every response
carries both goalBot and taskAgent (or goalBots and taskAgents) so integrations written
against either name keep working.The older stage-based engine moved to /api/instructions
and is being retired.A new bot is live the moment it is created and will start answering leads whose tags already
match. Pass
"status": "paused" if you want to create it, read it back, and switch it on
deliberately.Authentication
Pass your workspace API key as a Bearer token, or use a Clerk session token.Authorization: Bearer tuco_xxxxxxxxxxxxx
Request body
string
required
Display name for the bot. An empty or missing name returns
400 Name is required.string
required
The instructions the model runs on — who it is, what it is trying to do, what it must not say.
An empty or missing prompt returns
400 Prompt is required.string[]
Tags that wake this bot. A lead carrying any of them is handled by this bot when it replies.
Set tags with
PATCH /api/leads/{id}. Defaults to [].string[]
Tags that keep a lead away from this bot, even when
includeTags matches. Defaults to [].string
default:"autopilot"
How replies are delivered.
| Value | Behaviour |
|---|---|
autopilot | The bot replies on its own. |
copilot | Every reply is saved as a draft for a human to approve. |
manual | The bot is off. This is the only way to stop the built-in Auto Replies bot, which cannot be deleted. |
draft | Legacy alias for copilot, still accepted. |
An unrecognised value is not rejected — it is silently stored as
copilot. Read the
mode back from the response to confirm what was saved.string
default:"active"
active or paused. A new bot goes live the moment it exists and starts answering leads
whose tags already match, so pass "paused" when you want to create, inspect, then flip it on
with PATCH. Any value other than "paused" is
stored as active.string
default:"calendly"
Calendar used for booking:
calendly or ghl_calendar. Any other value falls back to calendly.string
default:"always"
When the bot searches your knowledge base.
always searches on every inbound message;
auto leaves the decision to the model. On create, anything other than "auto" is stored as always.string
Free-text tone hint, e.g.
"warm and brief".boolean
default:"true"
Whether the bot may ask the lead follow-up questions. Only an explicit
false turns it off.boolean
default:"true"
Send a typing signal before replying. Only an explicit
false turns it off.number
default:"8"
How many times the bot may reply in one conversation. Clamped to
1–20; a non-numeric or
non-positive value falls back to 8.string
Tag applied to the lead when the bot reaches its goal.
boolean
default:"false"
Push
onDoneTag to the connected CRM as well as to Tuco. Only an explicit true enables it.string[]
Ids of the API tools this bot may call, from
GET /api/campaign-tools.
Omit to allow none.string[]
Field names the bot should try to collect during the conversation, e.g.
["email", "company"].Example
curl -X POST "https://app.tuco.ai/api/goal-bots" \
-H "Authorization: Bearer tuco_xxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Demo follow-up",
"prompt": "You are Raj from Tuco. Book a 15-minute demo. Never quote a price.",
"includeTags": ["demo-requested"],
"excludeTags": ["customer"],
"mode": "copilot",
"status": "paused",
"maxTurns": 6,
"onDoneTag": "demo-booked",
"collectFields": ["email"]
}'
Success (200 OK)
{
"goalBot": {
"_id": "6ac3ed5f71adb32c88d566df",
"workspaceId": "org_3CzFir0Fps4hHL4nbU9WQ6FR4ur",
"name": "Demo follow-up",
"prompt": "You are Raj from Tuco. Book a 15-minute demo. Never quote a price.",
"connector": "calendly",
"includeTags": ["demo-requested"],
"excludeTags": ["customer"],
"mode": "copilot",
"kbRetrieval": "always",
"canAsk": true,
"maxTurns": 6,
"onDoneTag": "demo-booked",
"collectFields": ["email"],
"writeTagToCrm": false,
"typingIndicator": true,
"status": "paused",
"createdAt": "2026-10-05T18:33:03.511Z",
"updatedAt": "2026-10-05T18:33:03.511Z"
},
"taskAgent": { /* identical object */ }
}
The response is
200, not 201. Read goalBot._id and use it on every {id} route.Errors
| Status | When | Body |
|---|---|---|
400 | name missing or blank | { "error": "Name is required" } |
400 | prompt missing or blank | { "error": "Prompt is required" } |
400 | Key has no active workspace | { "error": "No active workspace" } |
401 | Missing or invalid API key | { "error": "Unauthorized" } |