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 — always true: 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) accepts network.locked: true and changes nothing, and answers network.locked: false with 400 invalid_request naming network.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_time or never_allow. Set by PATCH (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

StatusCodeWhenWhat to do
401unauthenticatedThe credential is missing, refused, or below the scope the route needs.Use a write key for the writes.
400invalid_requestA field is outside its bounds, a PATCH names no field, or the POST carries no Idempotency-Key.Read details.
404not_foundNo 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.
405not_supportedPOST /bots/{id}/restore: the product has no undelete.Create the bot again.
403forbiddenThe bot is not this person's to change.Ask its owner.
429quota_exceededThe account is at its ceiling of fifty bots and group chats together.Delete a bot first.
429rate_limitedA 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.