BetterClaw
Agent API

Agent API quickstart

Generate a key, start a chat, send a message, and read your agent's reply.

Quickstart

This walks through one full turn with curl: find your workspace and agent, start a chat, send a message, and read the reply. You need a workspace with at least one agent, and you need to be a workspace owner or admin to generate the key. The examples use jq to pick fields out of responses.

1. Generate a key

  1. In the sidebar, choose Agents under Workspace settings, then open the API keys tab (or go straight to /agents?tab=api-keys in the app).
  2. Choose Generate key.
  3. Name the key (for example Quickstart), leave the agent on Any agent, and under permissions select Read chats, Send messages, List agents, and Read workspace.
  4. Choose Generate, then copy the key. It starts with bc_sk_ and is only shown this once.

Put it in your shell so the commands below can use it:

export BETTERCLAW_API_KEY="bc_sk_…"

2. Find your workspace

curl -s https://api.betterclaw.io/workspaces \
  -H "Authorization: Bearer $BETTERCLAW_API_KEY"

A key only ever sees its own workspace, so the list has one entry:

[
  {
    "id": "6f1c2d4e-8a90-4b1c-9d2e-3f4a5b6c7d8e",
    "name": "Acme",
    "status": "running",
    "role": "owner",
    "plan": "pro",
    "createdAt": "2026-08-01T09:00:00.000Z"
  }
]
export WORKSPACE_ID="6f1c2d4e-8a90-4b1c-9d2e-3f4a5b6c7d8e"

3. Pick an agent

curl -s "https://api.betterclaw.io/workspaces/$WORKSPACE_ID/agents" \
  -H "Authorization: Bearer $BETTERCLAW_API_KEY" \
  | jq '.[] | {id, displayName, locked}'
{ "id": "0b7e9f3a-1c2d-4e5f-8a9b-0c1d2e3f4a5b", "displayName": "Scout", "locked": false }
export AGENT_ID="0b7e9f3a-1c2d-4e5f-8a9b-0c1d2e3f4a5b"

4. Start a chat

curl -s https://api.betterclaw.io/chats \
  -H "Authorization: Bearer $BETTERCLAW_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"workspaceId\": \"$WORKSPACE_ID\", \"agentId\": \"$AGENT_ID\", \"title\": \"Hello from the API\"}" \
  | jq '{id, agentName, title}'
{ "id": "9a8b7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d", "agentName": "Scout", "title": "Hello from the API" }
export CHAT_ID="9a8b7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d"

The chat appears in Chats in the app too, as one of yours.

5. Send a message

curl -s "https://api.betterclaw.io/chats/$CHAT_ID/messages" \
  -H "Authorization: Bearer $BETTERCLAW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"content": "In three bullet points, what can you help me with?"}' \
  | jq '.assistantMessage | {id, status, content}'

The request returns as soon as the message is accepted. The reply is written afterwards, so the assistant message comes back empty and streaming:

{ "id": "c3d4e5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f", "status": "streaming", "content": "" }
export MESSAGE_ID="c3d4e5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f"

6. Read the reply

Fetch the chat until that message is no longer streaming. Its content fills in as the agent writes, and ends as complete (or error, with an errorMessage):

while :; do
  msg=$(curl -s "https://api.betterclaw.io/chats/$CHAT_ID" \
    -H "Authorization: Bearer $BETTERCLAW_API_KEY" \
    | jq --arg id "$MESSAGE_ID" '.messages[] | select(.id == $id)')
  [ "$(echo "$msg" | jq -r .status)" != "streaming" ] && break
  sleep 2
done
echo "$msg" | jq -r '.status, .content'
If the agent was idle, it starts up before it answers, so the first reply can take a little longer than the ones after it.

7. Stream instead of polling

To show the reply as it's written, open the chat stream. This Node.js script (using the ws package) prints the reply to the message you send in step 5 as it arrives. Start it first, then send the message:

import WebSocket from 'ws';

const { BETTERCLAW_API_KEY, WORKSPACE_ID, CHAT_ID } = process.env;

const ws = new WebSocket(`wss://api.betterclaw.io/ws/chats?workspaceId=${WORKSPACE_ID}`, {
  headers: { Authorization: `Bearer ${BETTERCLAW_API_KEY}` },
});

let printed = '';
ws.on('message', (data) => {
  const frame = JSON.parse(data);
  if (frame.chatId !== CHAT_ID) return;
  // Live progress arrives as `message_streaming`; the saved reply as `message_upserted`.
  const reply =
    frame.type === 'message_streaming'
      ? frame
      : frame.type === 'message_upserted' && frame.message.role === 'assistant'
        ? frame.message
        : null;
  if (!reply) return;
  // `content` is the whole reply so far, not just the new part.
  process.stdout.write(reply.content.slice(printed.length));
  printed = reply.content;
  if (reply.status === 'error' && reply.errorMessage) console.error(`\n${reply.errorMessage}`);
  if (reply.status !== 'streaming') ws.close();
});
ws.on('close', (code, reason) => console.log(`\n[closed ${code} ${reason}]`));

See Streaming replies for every event the stream sends.

Next steps

  • Building in JavaScript or TypeScript? The JavaScript SDK (npm install @better-claw/sdk) does all of the above for you.
  • Calling from a browser or mobile app? Exchange the key for a session token on your server first.
  • Browse every request in the endpoint reference.
  • When you're done testing, revoke the quickstart key from the API keys tab.