Endpoint reference
Endpoint reference
All paths are relative to https://api.betterclaw.io and need an Authorization: Bearer header
with an API key or session token. IDs are UUIDs and timestamps are
ISO 8601 strings in UTC. List endpoints return everything in one response; there is no pagination.
A request that names a different workspace than the key's, through a path, query, or body
workspaceId, fails with 403. See Errors for every status code.
Workspaces
List workspaces
GET /workspaces · workspaces:read
Returns the key's own workspace, so the array has one entry. It's empty while the workspace is being set up or is in an error state.
[
{
"id": "6f1c2d4e-8a90-4b1c-9d2e-3f4a5b6c7d8e",
"name": "Acme",
"status": "running",
"role": "owner",
"plan": "pro",
"createdAt": "2026-08-01T09:00:00.000Z"
}
]
| Field | Description |
|---|---|
status | provisioning, initializing, running, stopping, stopped, or deleting. Agents answer when running. |
role | The key creator's role in the workspace: owner, admin, or member. |
plan | free, starter, pro, business, or enterprise. |
Get a workspace
GET /workspaces/{workspaceId} · workspaces:read
Returns id, name, status, and createdAt for the key's workspace.
Agents
List agents
GET /workspaces/{workspaceId}/agents · agents:read
Returns the workspace's agents in no particular order. A key pinned to an agent sees only that agent.
[
{
"id": "0b7e9f3a-1c2d-4e5f-8a9b-0c1d2e3f4a5b",
"workspaceId": "6f1c2d4e-8a90-4b1c-9d2e-3f4a5b6c7d8e",
"displayName": "Scout",
"name": "researcher",
"templateId": "researcher",
"soulMd": "# Soul\n\nYou are Scout, our eyes on the outside world…",
"provider": "anthropic",
"model": "anthropic/claude-sonnet-5",
"timezone": "Europe/London",
"avatarUrl": "data:image/webp;base64,UklGR…",
"memoryEnabled": true,
"locked": false,
"createdAt": "2026-08-01T09:02:11.000Z",
"updatedAt": "2026-09-30T14:20:05.000Z"
}
]
| Field | Description |
|---|---|
displayName | The agent's name as shown in the app. Use this, not name, when you show an agent to people. |
templateId | The template it was created from: researcher, coder, marketer, finance, writer, or assistant. |
soulMd | Its personality instructions (soul.md). |
provider, model | The LLM it runs on. |
timezone | IANA timezone, or null for UTC. |
avatarUrl | The avatar as a data: URL, or null. |
locked | true when the workspace's plan doesn't include this agent (Free keeps only the first). Messages to it fail with 402. |
Get an agent
GET /workspaces/{workspaceId}/agents/{agentId} · agents:read
Returns one agent, in the same shape as the list but without locked. An agent that doesn't
exist returns an empty response body.
List agent templates
GET /workspaces/{workspaceId}/agents/templates · agents:read
Returns the templates new agents are created from, each with id, name, displayName,
personality, description, icon, illustration, and color. Creating agents isn't part of
the Agent API; this is for showing what a templateId means.
Chats
A chat is a conversation with one agent. Chats created through the API appear in Chats in the app, as chats of the person who generated the key.
The chat object
{
"id": "9a8b7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d",
"userId": "2d3e4f5a-6b7c-4d8e-9f0a-1b2c3d4e5f6a",
"workspaceId": "6f1c2d4e-8a90-4b1c-9d2e-3f4a5b6c7d8e",
"agentId": "0b7e9f3a-1c2d-4e5f-8a9b-0c1d2e3f4a5b",
"agentName": "Scout",
"title": "Hello from the API",
"summary": null,
"archivedAt": null,
"pinned": false,
"createdAt": "2026-10-08T10:15:00.000Z",
"updatedAt": "2026-10-08T10:15:42.000Z"
}
| Field | Description |
|---|---|
userId | Who the chat belongs to: the person who generated the key, for chats the key starts. |
agentId | The agent. null if the agent has since been removed; agentName keeps its name. |
title | If you don't set one, it's generated from the first message. |
summary | { "headline": string, "keyResults": string[] } once the chat has been summarized, else null. |
pinned | Whether the key's user has pinned the chat. Not returned when creating a chat. |
archivedAt | When the chat was archived, or null. |
Chats also carry a few fields the app uses for sharing and summaries; don't build on fields that aren't listed here.
Create a chat
POST /chats · chats:write
curl -s https://api.betterclaw.io/chats \
-H "Authorization: Bearer $BETTERCLAW_API_KEY" \
-H "Content-Type: application/json" \
-d '{"workspaceId": "6f1c2d4e-…", "agentId": "0b7e9f3a-…", "title": "Weekly report"}'
| Field | Type | Required | Description |
|---|---|---|---|
workspaceId | uuid | Yes | The key's workspace. |
agentId | uuid | No | The agent to talk to. If you leave it out, BetterClaw picks one (see below). |
title | string | No | Up to 500 characters. |
routingMessage | string | No | The first message you're about to send, used only to pick an agent when agentId is left out. Not saved. |
clientRequestId | uuid | No | Makes the request safe to retry; see Retrying safely. |
Returns the new chat (201).
If you leave out agentId, the chat goes to the key's pinned agent if it has one. Otherwise, a
routingMessage that starts with an agent's name (@Scout …, or @"Research Analyst" … for
names with spaces) picks that agent, and anything else is routed to the best-suited agent. When a
mention was used, the response includes routingMessage with the mention removed, so you can send
that text as the first message.
List chats
GET /chats?workspaceId={workspaceId} · chats:read
Returns every chat in the workspace that hasn't been deleted, newest activity first, including archived chats and chats started by other members. Each item is a chat object.
Get a chat with its messages
GET /chats/{chatId} · chats:read
Returns the chat object plus messages, oldest first.
{
"id": "9a8b7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d",
"title": "Weekly report",
"messages": [
{
"id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
"chatId": "9a8b7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d",
"role": "user",
"status": "complete",
"content": "Draft this week's report from the attached numbers.",
"attachments": [{ "name": "numbers.csv", "mimeType": "text/csv", "bytes": 1832 }],
"deliverable": null,
"updates": [],
"question": null,
"errorMessage": null,
"turnIndex": 0,
"createdAt": "2026-10-08T10:15:01.000Z",
"updatedAt": "2026-10-08T10:15:01.000Z"
},
{
"id": "c3d4e5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f",
"chatId": "9a8b7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d",
"role": "assistant",
"status": "complete",
"content": "Here's the draft. Revenue is up 8% week over week…",
"attachments": [],
"deliverable": [{ "filename": "weekly-report.pdf", "mimeType": "application/pdf", "bytes": 48211, "version": 1 }],
"updates": [{ "timestamp": "2026-10-08T10:15:20.000Z", "message": "Reading numbers.csv" }],
"question": null,
"errorMessage": null,
"turnIndex": 1,
"createdAt": "2026-10-08T10:15:01.000Z",
"updatedAt": "2026-10-08T10:15:42.000Z"
}
]
}
| Field | Description |
|---|---|
role | user or assistant. |
status | streaming while the agent is still writing, then complete or error. |
content | The text. While streaming, the reply so far. |
attachments | Files sent with a user message: name, mimeType, bytes. |
deliverable | Files the agent produced, or null. Download one by its position in this array. |
updates | Progress notes the agent posted while working. |
question | Set when the agent stopped to ask you something: { id, text, suggestedAnswer?, expiresAt, status }. Answer by sending a message. |
errorMessage | Why the reply failed, when status is error. |
Update a chat
PATCH /chats/{chatId} · chats:write
Send at least one of:
| Field | Type | Description |
|---|---|---|
title | string | 1–500 characters. |
pinned | boolean | Pins the chat for the key's user. |
archived | boolean | true moves it to Archive, false restores it. |
Returns the updated chat.
Delete a chat
DELETE /chats/{chatId} · chats:write
Deletes the chat for everyone in the workspace and returns { "success": true }. It's removed
permanently after 30 days.
Messages
Send a message
POST /chats/{chatId}/messages · chats:write
Send JSON for text only:
curl -s "https://api.betterclaw.io/chats/$CHAT_ID/messages" \
-H "Authorization: Bearer $BETTERCLAW_API_KEY" \
-H "Content-Type: application/json" \
-d '{"content": "Summarize the top three stories today."}'
or multipart/form-data to attach files, with each file in a files field:
curl -s "https://api.betterclaw.io/chats/$CHAT_ID/messages" \
-H "Authorization: Bearer $BETTERCLAW_API_KEY" \
-F "content=Draft this week's report from these numbers." \
-F "files=@numbers.csv"
| Field | Type | Description |
|---|---|---|
content | string | The message, up to 100,000 characters. |
files | file | Attachments: up to 30 MB each, and 50 per chat in total. See accepted types below. |
clientRequestId | uuid | Makes the request safe to retry; see Retrying safely. |
Accepted file types: PNG, JPEG, GIF, and WebP images; PDF, Word, Excel, and PowerPoint documents; plain text, Markdown, CSV, HTML, XML, and JSON; MP3 and WAV audio; MP4, WebM, Ogg, MOV, AVI, MKV, MPEG, and 3GP video; Jupyter notebooks; ZIP archives; and RSS or Atom feeds. Files are checked by their contents, not just their extension.
A message needs text, a file, or both. The response (201) comes back as soon as the message is
accepted, before the agent replies:
{
"userMessage": { "id": "b2c3d4e5-…", "role": "user", "status": "complete", "content": "Summarize the top three stories today.", "turnIndex": 2 },
"assistantMessage": { "id": "c3d4e5f6-…", "role": "assistant", "status": "streaming", "content": "", "turnIndex": 3 }
}
The agent then writes into assistantMessage. To get the reply, either
stream it or fetch the chat until that message's status is no longer
streaming. A reply that stalls for an hour is marked error.
Stop a reply
POST /chats/{chatId}/messages/{messageId}/stop · chats:write
Stops the agent writing messageId. Returns { "ok": true, "stopped": true }, or
"stopped": false if the message had already finished. A stopped reply keeps what was written so
far and ends as complete.
Download a deliverable
GET /chats/{chatId}/messages/{messageId}/deliverables/{index} · chats:read
Downloads a file the agent produced. index is the file's position in the message's deliverable
array, starting at 0. The response is a 302 redirect to a download link that expires after 60
seconds, so follow redirects and don't store the link:
curl -sL -o weekly-report.pdf \
"https://api.betterclaw.io/chats/$CHAT_ID/messages/$MESSAGE_ID/deliverables/0" \
-H "Authorization: Bearer $BETTERCLAW_API_KEY"
| Query | Description |
|---|---|
preview=true | Redirects to a link that opens in the browser instead of downloading. |
inline=true | Returns the file's bytes directly instead of redirecting (files up to 50 MB). |
Retrying safely
POST /chats and POST /chats/{chatId}/messages accept a clientRequestId. Generate a UUID once
per logical request and reuse it when you retry after a timeout or network error. A retry with the
same ID and the same content returns the original result instead of creating a second chat or
sending the message twice. Reusing an ID for different content returns 409.