Bots
Developer·4 minutes to read
Verified against docs/superpowers/specs/2026-09-15-p4-a-typed-http-api-openapi-webhooks.md §4.3, §4.4, §5.1, §5.2, §6; re-verified against the code at close-out.
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 (F-BOT-1 to F-BOT-14 — 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).
Authentication
An organisation API key. GET needs scope read or wider; POST, PATCH and DELETE need write or wider. Every call runs as the admin 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 (F-APR-6).
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_ORIGIN/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_ORIGIN}/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 fifty-bot ceiling. | Delete a bot first. |
| 402 | payment_required | The organisation needs a trial or a payment. | Settle the account, then retry. |
| 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.