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

# Agent Chats API

> Send AI chat sessions and message feedback to BuildBetter

Use Agent Chats to analyze the quality of your AI product conversations. BuildBetter stores each conversation as a session, extracts qualitative Signals, and keeps explicit message feedback linked to the assistant response that received it.

Agent Chats focus on the user experience. They identify steering and correction, misunderstanding, repetition, capability gaps, missing context, incomplete outcomes, response-quality gaps, user friction, product opportunities, and explicit positive or negative feedback.

## Set up an application

<Steps>
  <Step title="Open Agent Chats">
    In BuildBetter, open **Library → Agent Chats**, then select **Set up Agent Chats**. Ask a workspace administrator if Agent Chats is not available in your workspace.
  </Step>

  <Step title="Create an application">
    Give the application a name and a stable key. Use one application for each agent or product whose conversations you want to filter separately.
  </Step>

  <Step title="Configure privacy">
    Set content capture, participant type, allowed environments, retention, redaction, and conversation access before you send production data.
  </Step>

  <Step title="Create an ingest credential">
    Copy the secret when BuildBetter shows it. The secret is shown once. Store it in a server-side secret manager and do not put it in browser code.
  </Step>
</Steps>

## Authentication

Send the ingest secret in `X-BuildBetter-Agent-Key`. This is an ingestion-only credential. It is different from your organization API key.

```http theme={null}
X-BuildBetter-Agent-Key: bb_agent_...
Content-Type: application/json
```

The request body must also contain the application `agentKey`. BuildBetter validates that the key and secret belong to the same application.

## Send a conversation

Send events to:

```http theme={null}
POST https://api.buildbetter.app/v3/rest/agent-conversations/ingest
```

The following example sends one user message and one finalized assistant response.

```bash theme={null}
curl "https://api.buildbetter.app/v3/rest/agent-conversations/ingest" \
  -H "Content-Type: application/json" \
  -H "X-BuildBetter-Agent-Key: $BUILDBETTER_AGENT_SECRET" \
  -d '{
    "agentKey": "support-agent",
    "externalSessionId": "chat_123",
    "status": "open",
    "agentVersion": "2026-08-26",
    "source": {
      "sdk": "vercel-ai",
      "version": "5.0.0"
    },
    "context": {
      "productArea": "analytics",
      "surface": "dashboard-assistant",
      "routeTemplate": "/dashboards/:dashboardId",
      "pathname": "/dashboards/db_42",
      "entryPoint": "ask-ai",
      "entityType": "dashboard",
      "entityId": "db_42",
      "environment": "production",
      "release": "web-2026.08.26",
      "properties": {
        "plan": "growth"
      }
    },
    "participants": [
      {
        "externalId": "user_42",
        "displayName": "Avery Chen",
        "username": "avery",
        "email": "avery@example.com",
        "companyExternalId": "account_17",
        "boundary": "external"
      }
    ],
    "events": [
      {
        "externalEventId": "user_1",
        "revision": 1,
        "sequence": 1,
        "occurredAt": "2026-08-26T18:00:00.000Z",
        "type": "user_message",
        "participantExternalId": "user_42",
        "content": "Why did revenue drop this month?"
      },
      {
        "externalEventId": "assistant_1",
        "revision": 1,
        "sequence": 2,
        "occurredAt": "2026-08-26T18:00:03.000Z",
        "type": "assistant_message",
        "final": true,
        "content": "Revenue fell because upgrades declined by 12%.",
        "model": {
          "provider": "openai",
          "name": "gpt-5"
        },
        "usage": {
          "inputTokens": 740,
          "outputTokens": 31,
          "latencyMs": 1840
        },
        "trace": {
          "traceId": "trace_abc",
          "spanId": "span_123"
        }
      }
    ]
  }'
```

BuildBetter returns the stable session ID and the event counts for the batch.

```json theme={null}
{
  "sessionId": "0198e42f-8027-7fd6-b514-8ee755e50590",
  "acceptedEventCount": 2,
  "deduplicatedEventCount": 0,
  "revisedEventCount": 0,
  "processingStatus": "pending"
}
```

`pending` means BuildBetter accepted the data and queued signal extraction. It does not mean extraction is complete.

## Session and event identity

BuildBetter uses `agentKey` plus `externalSessionId` as the stable session identity. Continue to send new events with the same values while the conversation is active.

Each event needs:

* `externalEventId`: a stable ID from your system.
* `sequence`: the event's position in the session. Sequence numbers must be unique.
* `revision`: start at `1`. Increase it when you correct an existing event.
* `occurredAt`: an ISO 8601 timestamp.
* `type`: such as `user_message`, `assistant_message`, or `feedback`.

Retries with the same sanitized event content are deduplicated. If the content changes, send a revision greater than the stored revision. A revision cannot change the event sequence or type, and a finalized event cannot become partial again.

Events in one request do not have to be in array order. BuildBetter uses `sequence` to order them. Open sessions wait at sequence gaps. Ended or abandoned sessions process through permanent gaps.

## Finalize a session

Send `status: "ended"` after the conversation ends. You can send only the final lifecycle update if the earlier events already exist, but every ingest request must contain at least one event. A common pattern is to include the last new event with the terminal status.

Use `status: "abandoned"` when the user leaves before the agent reaches an outcome.

## Send message feedback

Send feedback as its own event. `targetEventExternalId` must point to an earlier `assistant_message` in the same session.

```bash theme={null}
curl "https://api.buildbetter.app/v3/rest/agent-conversations/ingest" \
  -H "Content-Type: application/json" \
  -H "X-BuildBetter-Agent-Key: $BUILDBETTER_AGENT_SECRET" \
  -d '{
    "agentKey": "support-agent",
    "externalSessionId": "chat_123",
    "status": "open",
    "participants": [],
    "events": [
      {
        "externalEventId": "feedback_assistant_1",
        "revision": 1,
        "sequence": 3,
        "occurredAt": "2026-08-26T18:00:10.000Z",
        "type": "feedback",
        "feedback": {
          "targetEventExternalId": "assistant_1",
          "rating": "negative",
          "labels": ["incorrect", "missing-context"],
          "comment": "That compares this month with the wrong period."
        }
      }
    ]
  }'
```

Ratings are `positive` or `negative`. A feedback event can contain a rating, labels, a comment, or any combination of them. BuildBetter creates explicit-feedback Signals deterministically and does not use model credits for that conversion. The normal qualitative analysis of finalized assistant responses uses response credits.

In Signals, use the Agent Chat source filters to separate `explicit_feedback` from `qualitative_inference` and to filter positive or negative ratings.

## Vercel AI SDK

Normalize finalized UI messages in a server-side `onFinish` callback. Do not call the ingestion API directly from the browser because that would expose the ingest secret.

```ts theme={null}
onFinish: async ({ messages }) => {
  await fetch(
    "https://api.buildbetter.app/v3/rest/agent-conversations/ingest",
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-BuildBetter-Agent-Key": process.env.BUILDBETTER_AGENT_SECRET!,
      },
      body: JSON.stringify({
        agentKey: "support-agent",
        externalSessionId: chatId,
        status: "open",
        source: { sdk: "vercel-ai", version: "5" },
        context: {
          productArea: "support",
          surface: "help-center-chat",
          environment: process.env.NODE_ENV,
        },
        participants: [
          {
            externalId: user.id,
            displayName: user.name,
            email: user.email,
            boundary: "external",
          },
        ],
        events: messages.map((message, index) => ({
          externalEventId: message.id,
          revision: 1,
          sequence: index + 1,
          occurredAt: new Date(message.createdAt).toISOString(),
          type:
            message.role === "assistant"
              ? "assistant_message"
              : "user_message",
          final: true,
          participantExternalId:
            message.role === "user" ? user.id : undefined,
          content: message.parts,
        })),
      }),
    },
  );
};
```

For large sessions, send only new events after the first request. One request accepts at most 500 events and 2 MiB. One event accepts at most 256 KiB. Keep `occurredAt` stable across retries, as shown above.

## Context and filters

Set context at the session level when it applies to the whole chat. An event can also supply context to override the session defaults.

| Field                       | Use                                                                               |
| --------------------------- | --------------------------------------------------------------------------------- |
| `productArea`               | A stable product area, such as `analytics` or `crm`.                              |
| `surface`                   | The exact experience, such as `dashboard-assistant`, `bob`, or `copilot-sidebar`. |
| `routeTemplate`             | A low-cardinality route, such as `/dashboards/:dashboardId`.                      |
| `pathname`                  | The actual path where the chat occurred.                                          |
| `entryPoint`                | The control or flow that opened the chat.                                         |
| `entityType` and `entityId` | The product object in context.                                                    |
| `environment`               | `production`, `staging`, or another configured environment.                       |
| `release`                   | Your application or agent release.                                                |
| `properties`                | Up to 50 additional JSON values.                                                  |

These fields make Agent Chats filterable by agent, product area, surface, environment, and related object.

## Participant identity

Use a stable `externalId` for each participant. If you send `displayName`, `username`, or `email`, `externalId` is required. You can also send a BuildBetter `personId` UUID when you already know the canonical Person.

Set `boundary` to:

* `external` for a customer, prospect, or other product user.
* `internal` for a team member.

BuildBetter can use the identity and boundary in signal attribution. Configure redaction before you send personal data that your workspace does not need.

## Privacy and retention

Application settings apply before storage and hashing.

* **Content capture:** Turn it off to retain counts and status without message content.
* **Redacted field names:** Remove matching keys from message payloads, feedback, model metadata, usage, traces, context, and participant identity fields.
* **Redacted text patterns:** Replace exact sensitive text before storage.
* **Retention:** Set a value from 1 through 3650 days, or leave it blank for no application-specific limit.
* **Allowed environments:** Leave the list blank to accept all environments. If you configure a list, each request must resolve to an allowed environment.
* **Conversation access:** Use workspace access or restrict the application to selected BuildBetter user UUIDs.

BuildBetter also masks common secret fields, bearer tokens, private keys, long binary payloads, and common provider secret formats.

## Billing

BuildBetter meters `agent_response_processed` units. One visible finalized assistant response is one response unit. Appending a new response adds only the new unit. A successful idempotent retry does not add another unit.

Explicit positive or negative feedback creates its explicit-feedback Signal without model credits. The session can still use credits when BuildBetter runs normal qualitative analysis on finalized assistant responses.

## Coding-agent sessions and Projects through MCP

The MCP Agent Session tools are for uploaded coding-agent session artifacts, such as Codex or Claude Code sessions. They are separate from the Agent Chats ingestion endpoint above.

To create a Projects V2 project and attach an uploaded coding-agent session:

1. Call `create-project` and keep the returned project `publicId`.
2. Call `upload-agent-session` with either normalized events or supported native JSONL.
3. Call `associate-agent-session` with `resourceType: "project"`, the project `publicId`, and the uploaded session ID.
4. Call `get-project` or `list-associated-agent-sessions` to verify the association.

MCP upload, project creation, and association are mutating actions. They require OAuth user context and explicit user approval.

## Troubleshooting

| Response | Meaning                                                                                                                             |
| -------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `400`    | The payload is invalid or too large, the environment is missing or blocked, or feedback does not target an earlier assistant event. |
| `401`    | The ingest secret is missing, revoked, or does not match the application key.                                                       |
| `404`    | Agent Chats is not enabled for the workspace or the application does not exist.                                                     |
| `409`    | An event changed without a higher revision, moved to another sequence, changed type, or reused an occupied sequence.                |

If a retry returns a high `deduplicatedEventCount`, the API is working as intended. If `processingStatus` is `billing_blocked`, add credits or adjust the relevant quota. If it is `capture_disabled`, enable content capture in the application settings before you send more content.
