BetterClaw
Agent API

Endpoint reference

Every Agent API request, its permission, parameters, and response.

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"
  }
]
FieldDescription
statusprovisioning, initializing, running, stopping, stopped, or deleting. Agents answer when running.
roleThe key creator's role in the workspace: owner, admin, or member.
planfree, 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"
  }
]
FieldDescription
displayNameThe agent's name as shown in the app. Use this, not name, when you show an agent to people.
templateIdThe template it was created from: researcher, coder, marketer, finance, writer, or assistant.
soulMdIts personality instructions (soul.md).
provider, modelThe LLM it runs on.
timezoneIANA timezone, or null for UTC.
avatarUrlThe avatar as a data: URL, or null.
lockedtrue when the workspace's plan doesn't include this agent (Free keeps only the first). Messages to it fail with 402.
Agent objects also carry internal fields that aren't listed here. They can change or disappear without notice, so don't build on them.

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"
}
FieldDescription
userIdWho the chat belongs to: the person who generated the key, for chats the key starts.
agentIdThe agent. null if the agent has since been removed; agentName keeps its name.
titleIf 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.
pinnedWhether the key's user has pinned the chat. Not returned when creating a chat.
archivedAtWhen 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"}'
FieldTypeRequiredDescription
workspaceIduuidYesThe key's workspace.
agentIduuidNoThe agent to talk to. If you leave it out, BetterClaw picks one (see below).
titlestringNoUp to 500 characters.
routingMessagestringNoThe first message you're about to send, used only to pick an agent when agentId is left out. Not saved.
clientRequestIduuidNoMakes 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"
    }
  ]
}
FieldDescription
roleuser or assistant.
statusstreaming while the agent is still writing, then complete or error.
contentThe text. While streaming, the reply so far.
attachmentsFiles sent with a user message: name, mimeType, bytes.
deliverableFiles the agent produced, or null. Download one by its position in this array.
updatesProgress notes the agent posted while working.
questionSet when the agent stopped to ask you something: { id, text, suggestedAnswer?, expiresAt, status }. Answer by sending a message.
errorMessageWhy the reply failed, when status is error.

Update a chat

PATCH /chats/{chatId} · chats:write

Send at least one of:

FieldTypeDescription
titlestring1–500 characters.
pinnedbooleanPins the chat for the key's user.
archivedbooleantrue 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"
FieldTypeDescription
contentstringThe message, up to 100,000 characters.
filesfileAttachments: up to 30 MB each, and 50 per chat in total. See accepted types below.
clientRequestIduuidMakes 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"
QueryDescription
preview=trueRedirects to a link that opens in the browser instead of downloading.
inline=trueReturns 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.