Bots
Developer·4 minutes to read
What it does
The bots your key's person can see: list them, create one from a starter template or from a full profile, read one, change its profile and settings, and delete it (the bot record, creation from a template, the profile editor, the fifty-bot ceiling, team bots, and the delete that is always a soft delete).
The bot network setting and the local-execution ceiling
The organisation-wide bot settings live on /api/v1/bot-settings, not on /bots. Two of its fields:
network.locked— alwaystrue: members never change the network setting in this product, only an administrator does.GET /api/v1/bot-settings(read key) returns it;PATCH /api/v1/bot-settings(admin key) acceptsnetwork.locked: trueand changes nothing, and answersnetwork.locked: falsewith 400invalid_requestnamingnetwork.locked, writing nothing from that request.capabilities.localExecutionCeiling— the most any member's computer may allow a bot to run locally:always_allow(no ceiling),ask_every_timeornever_allow. Set byPATCH(admin key). A machine set above it runs at the ceiling, and a member cannot set a machine above it. Organisation → Policies shows the same value: an administrator changes it there, a member sees it read-only.
Authentication
An organisation API key. GET needs scope read or wider; POST, PATCH and DELETE need write or wider. Every call runs as the person the key is bound to, so the bots it lists are exactly the bots that person sees in the app: their own, and the team bots a colleague has shared. A bot another member owns and has not shared is not_found here, as it is invisible there. See Authentication and keys.
Request
GET /bots
<!-- generated:begin GET /bots --> <!-- generated:end GET /bots -->POST /bots
<!-- generated:begin POST /bots --> <!-- generated:end POST /bots -->GET /bots/{id}
<!-- generated:begin GET /bots/{id} --> <!-- generated:end GET /bots/{id} -->PATCH /bots/{id}
<!-- generated:begin PATCH /bots/{id} --> <!-- generated:end PATCH /bots/{id} -->DELETE /bots/{id}
<!-- generated:begin DELETE /bots/{id} --> <!-- generated:end DELETE /bots/{id} -->POST /bots/{id}/restore
<!-- generated:begin POST /bots/{id}/restore --> <!-- generated:end POST /bots/{id}/restore -->POST /bots takes one of two shapes. from: "template" names a starter template or a published template share and optionally a name; from: "profile" carries the name (1 to 60 characters), an optional label, a description of up to 4 000 characters, a model tier, an avatar shape and colour, and a voice. A PATCH carries at least one field and changes only what it names. A description is a human-only write: it is the bot's standing rules, and no bot may write it.
DELETE /bots/{id} is a soft delete and answers the row with deletedAt set. GET /bots?includeDeleted=true adds the soft-deleted bots the person owns. There is no undelete in the product, so POST /bots/{id}/restore answers not_supported (405): the route exists so the verb table is complete and the answer is a decision rather than a missing page.
curl -sS -X POST "$BOTSEON_API_URL/api/v1/bots" \
-H "authorization: Bearer $BOTSEON_API_KEY" \
-H "idempotency-key: 4f1c0a2e-0d5b-4d3a-9a6f-2f1c0a2e0d5b" \
-H "content-type: application/json" \
-d '{"from":"template","templateId":"inbox-triage","name":"Inbox Triage"}'
const res = await fetch(`${process.env.BOTSEON_API_URL}/api/v1/bots`, {
method: 'POST',
headers: {
authorization: `Bearer ${process.env.BOTSEON_API_KEY}`,
'idempotency-key': crypto.randomUUID(),
'content-type': 'application/json',
},
body: JSON.stringify({ from: 'template', templateId: 'inbox-triage', name: 'Inbox Triage' }),
});
if (res.status !== 201) throw new Error(`bots: ${res.status}`);
const bot = (await res.json()) as { id: string; name: string; visibility: 'owner' | 'team' };
console.log(bot.id, bot.name, bot.visibility);
Response
{
"id": "0f3a2b71-6c4d-4a1e-9b2c-7d5e8f0a1b2c",
"name": "Inbox Triage",
"title": null,
"description": "Triages the inbox: labels what's urgent, drafts replies to the routine messages, and leaves the rest for a human to decide. Never sends a reply without approval.",
"tier": "balanced",
"visibility": "owner",
"avatar": { "shape": "rounded-square", "colour": "teal" },
"voice": null,
"templateId": "inbox-triage",
"ownerUserId": "8c1d4e5f-2a3b-4c5d-6e7f-8a9b0c1d2e3f",
"notifyOnFinish": true,
"maxSpendMicroCents": null,
"createdAt": "2026-09-18T08:00:00.000Z",
"deletedAt": null
}
tier is one of the product's seven model tiers. avatar.shape is one of eight shapes and avatar.colour one of eleven colours, both the editor's own lists. visibility is the column's own two values, owner or team, unmapped. maxSpendMicroCents is an integer in micro-cents, as every money field in this API is; there are no decimals anywhere on the wire.
Errors
| Status | Code | When | What to do |
|---|---|---|---|
| 401 | unauthenticated | The credential is missing, refused, or below the scope the route needs. | Use a write key for the writes. |
| 400 | invalid_request | A field is outside its bounds, a PATCH names no field, or the POST carries no Idempotency-Key. | Read details. |
| 404 | not_found | No such bot is visible to this key's person, or the template id matches neither a starter template nor a published share. | List first, and check the template id. |
| 405 | not_supported | POST /bots/{id}/restore: the product has no undelete. | Create the bot again. |
| 403 | forbidden | The bot is not this person's to change. | Ask its owner. |
| 429 | quota_exceeded | The account is at its ceiling of fifty bots and group chats together. | Delete a bot first. |
| 429 | rate_limited | A bucket is full. | Wait for Retry-After. |
Rate limits
Every call on this page is counted against two one-minute buckets: 300 requests per key and 1 000 per organisation. A full bucket answers rate_limited with Retry-After: 60; wait that many seconds and retry. Failed authentication is counted separately, against the caller's address, so a working key's allowance is never spent by somebody else's bad credential. See Envelopes, errors, idempotency and paging.
Last verified against build c0f77aa.