> ## Documentation Index
> Fetch the complete documentation index at: https://docs.brainstormer.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Anonymous Chat

> POST /api/public/agents/:slug/* — chat with a provisioned agent from the browser, no API key. Slug-based, anonymousId sessions, optional SSE streaming.

# Anonymous Chat

Once a build is `ready`, visitors chat with the agent using its public `slug`.
This is the **existing anonymous public surface** — **no API key**, slug-based,
with `anonymousId` sessions and optional SSE streaming. Because no secret is
involved, the **browser calls these endpoints directly** (no backend proxy
needed for chat).

<Note>
  `anonymousId` **MUST be a UUID** (e.g. `crypto.randomUUID()`). Generate one per
  visitor, persist it (e.g. `localStorage`), and reuse it across the conversation.
</Note>

***

## Create Conversation

<ParamField method="POST" path="/api/public/agents/:slug/conversations" />

Start a conversation and (if configured) receive a welcome message.

### Path Parameters

<ParamField path="slug" type="string" required>
  The agent's public slug — the `agentSlug` from the `ready` event.
</ParamField>

### Request Body

<ParamField body="anonymousId" type="string" required>
  A UUID identifying the anonymous visitor session.
</ParamField>

### Response (200)

<ResponseField name="success" type="boolean">Request status.</ResponseField>

<ResponseField name="data" type="object">
  <Expandable title="data object">
    <ResponseField name="conversationId" type="string">New conversation UUID.</ResponseField>

    <ResponseField name="welcomeMessage" type="string">
      Welcome message content, if the agent has one configured.
    </ResponseField>
  </Expandable>
</ResponseField>

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://app.brainstormer.io/api/public/agents/the-channel-agent/conversations \
    -H "Content-Type: application/json" \
    -d '{ "anonymousId": "550e8400-e29b-41d4-a716-446655440000" }'
  ```

  ```typescript Browser / TypeScript theme={null}
  const anonymousId =
    localStorage.getItem("bsio_anon") ??
    (() => {
      const id = crypto.randomUUID();
      localStorage.setItem("bsio_anon", id);
      return id;
    })();

  const res = await fetch(
    `https://app.brainstormer.io/api/public/agents/${slug}/conversations`,
    {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ anonymousId }),
    },
  );
  const { data } = await res.json();
  const { conversationId, welcomeMessage } = data;
  ```
</CodeGroup>

**Response (200):**

```json theme={null}
{
  "success": true,
  "data": {
    "conversationId": "c1a2b3c4-d5e6-7f80-9a1b-2c3d4e5f6071",
    "welcomeMessage": "Hey! Ask me anything about my videos."
  }
}
```

***

## Send Message

<ParamField method="POST" path="/api/public/agents/:slug/chat" />

Send a message in a conversation. Returns a full response, or an SSE token stream
when `stream: true`.

### Path Parameters

<ParamField path="slug" type="string" required>The agent's public slug.</ParamField>

### Request Body

<ParamField body="message" type="string" required>The visitor's message.</ParamField>

<ParamField body="conversationId" type="string" required>
  The UUID returned by create-conversation.
</ParamField>

<ParamField body="anonymousId" type="string" required>
  The same UUID used to create the conversation.
</ParamField>

<ParamField body="stream" type="boolean" default="false">
  Set `true` to receive an SSE token stream instead of a single JSON response.
</ParamField>

<ParamField body="attachments" type="array">
  Up to 5 files to attach to this message. Each item is `{ id, accessToken }`
  returned by [Upload Attachment](#upload-attachment). Only available when the
  agent has file uploads enabled.
</ParamField>

### Response (200) — non-streaming

<ResponseField name="response" type="string">The agent's reply.</ResponseField>
<ResponseField name="conversationId" type="string">The conversation UUID.</ResponseField>
<ResponseField name="usage" type="object">Token usage and cost info.</ResponseField>

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://app.brainstormer.io/api/public/agents/the-channel-agent/chat \
    -H "Content-Type: application/json" \
    -d '{
      "message": "What gear do you recommend?",
      "conversationId": "c1a2b3c4-d5e6-7f80-9a1b-2c3d4e5f6071",
      "anonymousId": "550e8400-e29b-41d4-a716-446655440000"
    }'
  ```

  ```typescript Browser / TypeScript theme={null}
  const res = await fetch(
    `https://app.brainstormer.io/api/public/agents/${slug}/chat`,
    {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        message: "What gear do you recommend?",
        conversationId,
        anonymousId,
      }),
    },
  );
  const data = await res.json();
  console.log(data.response);
  ```
</CodeGroup>

### Streaming (SSE)

Add `"stream": true` to receive a token stream. The browser can read it directly
with a `fetch` body reader:

```typescript Browser — streaming theme={null}
const res = await fetch(
  `https://app.brainstormer.io/api/public/agents/${slug}/chat`,
  {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ message, conversationId, anonymousId, stream: true }),
  },
);

const reader = res.body!.getReader();
const decoder = new TextDecoder();
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  for (const line of decoder.decode(value).split("\n")) {
    if (!line.startsWith("data: ")) continue;
    const evt = JSON.parse(line.slice(6));
    if (evt.token) process.stdout.write(evt.token); // append to the UI
    if (evt.done) console.log("\n", evt.usage);
  }
}
```

***

## Upload Attachment

<ParamField method="POST" path="/api/public/agents/:slug/attachments" />

Upload a file from the browser to attach to a chat message. Anonymous and
slug-scoped — **no API key**. Only works when the agent has **file uploads
enabled**; otherwise returns `403`. Send the file as `multipart/form-data` under
the field name `file`, with the visitor's `anonymousId` in the query string.

### Path Parameters

<ParamField path="slug" type="string" required>The agent's public slug.</ParamField>

### Query Parameters

<ParamField query="anonymousId" type="string" required>
  The visitor's UUID (the same one used for conversations).
</ParamField>

### Request

`multipart/form-data` with a single `file` part. Limits: max 5 files per message,
size + type enforced server-side (images, PDF, text, audio, video). Content is
magic-byte sniffed, so a file whose bytes don't match its declared type is
rejected (`415`).

### Response (200)

<ResponseField name="attachment" type="object">
  `{ id, accessToken, fileType, originalFilename, fileSizeBytes, contentType,
      processingStatus, createdAt }`. Pass `{ id, accessToken }` in the chat
  message's `attachments` array.
</ResponseField>

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST "https://app.brainstormer.io/api/public/agents/the-channel-agent/attachments?anonymousId=550e8400-e29b-41d4-a716-446655440000" \
    -F "file=@./photo.png"
  ```

  ```typescript Browser / TypeScript theme={null}
  // 1. Upload, then 2. send the returned ref with the next message.
  async function uploadAndSend(slug, anonymousId, conversationId, file, message) {
    const form = new FormData();
    form.append("file", file);
    const up = await fetch(
      `https://app.brainstormer.io/api/public/agents/${slug}/attachments?anonymousId=${anonymousId}`,
      { method: "POST", body: form }, // do NOT set Content-Type — the browser adds the boundary
    );
    if (!up.ok) throw new Error(`Upload failed (${up.status})`);
    const { attachment } = await up.json();

    return fetch(`https://app.brainstormer.io/api/public/agents/${slug}/chat`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        message,
        conversationId,
        anonymousId,
        attachments: [{ id: attachment.id, accessToken: attachment.accessToken }],
      }),
    });
  }
  ```
</CodeGroup>

<Note>
  Uploads are rate-limited per IP and per `anonymousId`. The `accessToken` is a
  per-upload secret returned only to the uploader — it's the ownership proof when
  you reference the attachment in a message, so don't expose it elsewhere.
</Note>

***

## List Conversations

<ParamField method="GET" path="/api/public/agents/:slug/conversations" />

List the conversations belonging to an anonymous visitor for this agent.

### Path Parameters

<ParamField path="slug" type="string" required>The agent's public slug.</ParamField>

### Query Parameters

<ParamField query="anonymousId" type="string" required>
  The visitor's UUID (the same one used to create the conversations).
</ParamField>

<ParamField query="limit" type="number" default="20">Maximum results.</ParamField>
<ParamField query="offset" type="number" default="0">Pagination offset.</ParamField>

### Response (200)

<ResponseField name="conversations" type="object[]">Array of the visitor's conversations.</ResponseField>

<ResponseField name="pagination" type="object">
  <Expandable title="pagination object">
    <ResponseField name="limit" type="number" />

    <ResponseField name="offset" type="number" />

    <ResponseField name="total" type="number" />

    <ResponseField name="hasMore" type="boolean" />
  </Expandable>
</ResponseField>

<CodeGroup>
  ```bash curl theme={null}
  curl "https://app.brainstormer.io/api/public/agents/the-channel-agent/conversations?anonymousId=550e8400-e29b-41d4-a716-446655440000&limit=20&offset=0"
  ```

  ```typescript Browser / TypeScript theme={null}
  const res = await fetch(
    `https://app.brainstormer.io/api/public/agents/${slug}/conversations?anonymousId=${anonymousId}&limit=20&offset=0`,
  );
  const { conversations, pagination } = await res.json();
  ```
</CodeGroup>

***

## Get Messages

<ParamField method="GET" path="/api/public/agents/:slug/conversations/:conversationId/messages" />

Retrieve the messages in one of the visitor's conversations.

### Path Parameters

<ParamField path="slug" type="string" required>The agent's public slug.</ParamField>
<ParamField path="conversationId" type="string" required>The conversation UUID.</ParamField>

### Query Parameters

<ParamField query="anonymousId" type="string" required>
  The visitor's UUID (the same one used to create the conversation).
</ParamField>

<ParamField query="limit" type="number" default="50">Maximum results.</ParamField>
<ParamField query="offset" type="number" default="0">Pagination offset.</ParamField>

### Response (200)

<ResponseField name="messages" type="object[]">Array of message objects in the conversation.</ResponseField>

<CodeGroup>
  ```bash curl theme={null}
  curl "https://app.brainstormer.io/api/public/agents/the-channel-agent/conversations/c1a2b3c4-d5e6-7f80-9a1b-2c3d4e5f6071/messages?anonymousId=550e8400-e29b-41d4-a716-446655440000"
  ```

  ```typescript Browser / TypeScript theme={null}
  const res = await fetch(
    `https://app.brainstormer.io/api/public/agents/${slug}/conversations/${conversationId}/messages?anonymousId=${anonymousId}`,
  );
  const { messages } = await res.json();
  ```
</CodeGroup>

***

## Rename Conversation

<ParamField method="PATCH" path="/api/public/agents/:slug/conversations/:conversationId" />

Rename one of the visitor's conversations.

### Path Parameters

<ParamField path="slug" type="string" required>The agent's public slug.</ParamField>
<ParamField path="conversationId" type="string" required>The conversation UUID.</ParamField>

### Request Body

<ParamField body="anonymousId" type="string" required>
  The visitor's UUID (the same one used to create the conversation).
</ParamField>

<ParamField body="title" type="string" required>
  The new conversation title. 1–80 characters.
</ParamField>

### Response (200)

<ResponseField name="conversation" type="object">The updated conversation object.</ResponseField>

<CodeGroup>
  ```bash curl theme={null}
  curl -X PATCH https://app.brainstormer.io/api/public/agents/the-channel-agent/conversations/c1a2b3c4-d5e6-7f80-9a1b-2c3d4e5f6071 \
    -H "Content-Type: application/json" \
    -d '{
      "anonymousId": "550e8400-e29b-41d4-a716-446655440000",
      "title": "Camera gear questions"
    }'
  ```

  ```typescript Browser / TypeScript theme={null}
  const res = await fetch(
    `https://app.brainstormer.io/api/public/agents/${slug}/conversations/${conversationId}`,
    {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ anonymousId, title: "Camera gear questions" }),
    },
  );
  const { conversation } = await res.json();
  ```
</CodeGroup>

***

## Delete Conversation

<ParamField method="DELETE" path="/api/public/agents/:slug/conversations/:conversationId" />

Delete one of the visitor's conversations. This is how a visitor clears one
conversation from their history.

### Path Parameters

<ParamField path="slug" type="string" required>The agent's public slug.</ParamField>
<ParamField path="conversationId" type="string" required>The conversation UUID.</ParamField>

### Query Parameters

<ParamField query="anonymousId" type="string" required>
  The visitor's UUID (the same one used to create the conversation).
</ParamField>

### Response (200)

<ResponseField name="success" type="boolean">`true` on success.</ResponseField>

<CodeGroup>
  ```bash curl theme={null}
  curl -X DELETE "https://app.brainstormer.io/api/public/agents/the-channel-agent/conversations/c1a2b3c4-d5e6-7f80-9a1b-2c3d4e5f6071?anonymousId=550e8400-e29b-41d4-a716-446655440000"
  ```

  ```typescript Browser / TypeScript theme={null}
  const res = await fetch(
    `https://app.brainstormer.io/api/public/agents/${slug}/conversations/${conversationId}?anonymousId=${anonymousId}`,
    { method: "DELETE" },
  );
  const { success } = await res.json();
  ```
</CodeGroup>

***

## Operator Presence (SSE)

<ParamField method="GET" path="/api/public/agents/:slug/conversations/:conversationId/events" />

Subscribe to real-time operator-presence / human-in-the-loop events for a
conversation — operator messages, takeover/join, and resolution. Use
`EventSource` (the `anonymousId` rides in the query string since EventSource
can't set headers); the server validates that the visitor owns the conversation.

### Query Parameters

<ParamField query="anonymousId" type="string" required>
  The visitor's UUID (must own the conversation).
</ParamField>

### Stream

`text/event-stream`. The first event is `{"type":"connected"}`; subsequent
`data:` events carry operator/HITL payloads (e.g. `{"type":"operator_message",
...}`, `{"type":"operator_joined", ...}`, `{"type":"hitl_resolved", ...}`).

```typescript Browser / TypeScript theme={null}
const es = new EventSource(
  `https://app.brainstormer.io/api/public/agents/${slug}/conversations/${conversationId}/events?anonymousId=${anonymousId}`,
);
es.addEventListener("message", (e) => {
  const evt = JSON.parse(e.data);
  if (evt.type === "operator_message") appendOperatorMessage(evt);
});
// es.close() when the conversation view unmounts.
```

<Note>
  This is the visitor side. The chat-core `PublicTransport.subscribeEvents` wires
  it automatically. The operator side (sending messages, taking over) runs through
  the authenticated HITL operator dashboard.
</Note>

***

## Notes

* The browser calls these endpoints **directly** — no `brs_live_` key, no
  backend proxy. Only [provisioning](/api-reference/provisioning/from-url) is
  gated.
* Any per-visitor message cap (e.g. an email gate after N messages) is a **client
  UX concern**, not an API limit.
* Seed the chat UI with the `starterQuestions` from the `ready`
  [`ProvisioningEvent`](/api-reference/provisioning/build-events-sse#provisioningevent-contract).
