> ## 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.

# Build Progress (SSE)

> GET /v1/agents/builds/:buildId/events — the primary, push-based progress stream. Replays current state, streams one event per phase transition, closes on a terminal event.

# Build Progress (SSE)

The **primary** way to observe a build. Open a
[Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)
stream and receive one [`ProvisioningEvent`](#provisioningevent-contract) per
phase transition plus progress ticks in slow phases. On connect, the current
state is **replayed** first, then events stream until a terminal event (`done`
or `error`), after which the connection closes.

<ParamField method="GET" path="/v1/agents/builds/:buildId/events" />

## Authentication

| Header          | Value                      | Required    |
| --------------- | -------------------------- | ----------- |
| `Authorization` | `Bearer brs_live_YOUR_KEY` | Yes         |
| `Accept`        | `text/event-stream`        | Recommended |

<Warning>
  This stream requires the `Authorization` header, so the browser's native
  `EventSource` (which **cannot send custom headers**) cannot consume it directly.
  Consume it **server-side** — your backend opens the stream with the key and
  **relays** events to the browser (over your own SSE/WebSocket). This is the
  [Pattern A backend proxy](/developer/provisioning-quickstart#pattern-a-backend-proxy).
  Use `fetch` with a streaming body reader, or a Node SSE client like
  [`eventsource`](https://www.npmjs.com/package/eventsource), not raw `EventSource`
  in the browser.
</Warning>

## Path Parameters

<ParamField path="buildId" type="string" required>
  The `bld_…` id returned by `from-url` (also available as the `streamUrl`).
</ParamField>

## Wire Format

Each message is a standard SSE frame. The `id:` is the event's `emittedAt`
ISO8601 timestamp (usable as `Last-Event-ID` for reconnect). The `event:` name is
`progress` for non-terminal events, `done` for a successful terminal, or `error`
for a failed terminal. The `data:` is a JSON-encoded `ProvisioningEvent`.

```
id: 2026-06-14T01:36:52.957Z
event: progress
data: {"buildId":"bld_8c9d0e1f2a3b","status":"analyzing","progress":35,"step":"analyzing","stepLabel":"Learning the creator's voice","detail":"Reading recent videos"}

id: 2026-06-14T01:37:40.110Z
event: progress
data: {"buildId":"bld_8c9d0e1f2a3b","status":"indexing","progress":80,"step":"indexing","stepLabel":"Indexing content","detail":"Indexed 6 of 8 videos"}

id: 2026-06-14T01:38:10.221Z
event: done
data: {"buildId":"bld_8c9d0e1f2a3b","status":"ready","progress":100,"step":"ready","stepLabel":"Your agent is ready","agentSlug":"the-channel-agent","starterQuestions":["What videos have you made about productivity?","Summarize your latest upload"],"emittedAt":"2026-06-14T01:38:10.221Z"}
```

A failed build closes with an `error` event:

```
event: error
data: {"buildId":"bld_8c9d0e1f2a3b","status":"failed","step":"scraping","progress":20,"stepLabel":"Couldn't read the channel","error":{"code":"private_or_empty","message":"Channel exists but has no public content to index","retryable":false},"emittedAt":"2026-06-14T01:36:00.000Z"}
```

## ProvisioningEvent Contract

A single shape returned by **SSE `data`, the
[poll snapshot](/api-reference/provisioning/get-build), and the
[webhook](/api-reference/provisioning/webhook)** — render against this one
contract everywhere.

```ts theme={null}
type ProvisioningStatus =
  | "queued"        // accepted, waiting for a worker
  | "detecting"     // normalizing URL + platform detection
  | "scraping"      // fetching channel preview content
  | "analyzing"     // building profile + system prompt (slow, ~80s)
  | "provisioning"  // creating KB + agent, publishing
  | "indexing"      // embedding + vector upsert of preview items
  | "ready"         // TERMINAL — agent published + index complete, slug live
  | "failed";       // TERMINAL — see error

interface ProvisioningEvent {
  buildId: string;
  status: ProvisioningStatus;
  progress: number;            // 0..100, monotonic, phase-weighted
  step: ProvisioningStatus;    // current phase
  stepLabel: string;           // display copy, e.g. "Reading recent videos"
  detail?: string;             // sub-status, e.g. "Indexed 6 of 8 videos"
  platform?: "youtube" | "instagram";
  channelTitle?: string;       // for "Building <X>…"
  agentSlug?: string;          // only on "ready"
  starterQuestions?: string[]; // only on "ready" — seed the chat UI
  warning?: string;            // soft issue, e.g. "Limited public content found"
  error?: { code: ProvisioningErrorCode; message: string; retryable: boolean };
  emittedAt: string;           // ISO8601 — also the SSE id:
}
```

### ProvisioningStatus values

| Status         | Phase                                                       | Terminal |
| -------------- | ----------------------------------------------------------- | -------- |
| `queued`       | Accepted, waiting for a worker.                             | —        |
| `detecting`    | Normalizing URL + platform detection.                       | —        |
| `scraping`     | Fetching channel preview content.                           | —        |
| `analyzing`    | Building the creator profile + system prompt (slow, \~80s). | —        |
| `provisioning` | Creating the KB + agent, publishing.                        | —        |
| `indexing`     | Embedding + vector upsert of preview items.                 | —        |
| `ready`        | Agent published **and** index complete, slug live.          | ✅        |
| `failed`       | Build failed — see `error`.                                 | ✅        |

<Note>
  `ready` is emitted **only** when the agent is published **and** its bounded index
  run is complete (or a phase timeout elapsed) — so the first chat is grounded in
  the creator's content. Thin / partial content still publishes and reaches `ready`
  with a `warning` rather than failing.
</Note>

### ProvisioningErrorCode table

Each error carries `retryable` so the client can branch (retry vs. explain).

```ts theme={null}
type ProvisioningErrorCode =
  | "unsupported_url"
  | "private_or_empty"
  | "scrape_failed"
  | "analysis_failed"
  | "indexing_failed"
  | "turnstile_failed"
  | "rate_limited"
  | "plan_limit"
  | "insufficient_credits"
  | "internal";
```

| Code                   | Meaning                                            | `retryable` | Client action                                |
| ---------------------- | -------------------------------------------------- | ----------- | -------------------------------------------- |
| `unsupported_url`      | Not a YouTube/Instagram channel we can read.       | `false`     | Ask for a different URL.                     |
| `private_or_empty`     | Channel exists but has no public content to index. | `false`     | Explain; ask for a public/different channel. |
| `scrape_failed`        | Upstream scrape error.                             | `true`      | Retry with backoff.                          |
| `analysis_failed`      | LLM error during analysis.                         | `true`      | Retry with backoff.                          |
| `indexing_failed`      | Embedding / vector error.                          | `true`      | Retry with backoff.                          |
| `turnstile_failed`     | Bot-check rejected.                                | `false`     | Get a fresh Turnstile token, resubmit.       |
| `rate_limited`         | Quota hit.                                         | `true`      | Back off, retry after `Retry-After`.         |
| `plan_limit`           | Org agent/KB plan limit reached.                   | `false`     | Surface plan-limit messaging.                |
| `insufficient_credits` | Org credit balance exhausted.                      | `false`     | Surface top-up / cap messaging.              |
| `internal`             | Unexpected platform error.                         | `true`      | Retry; escalate if persistent.               |

<Note>
  Pre-flight failures (`plan_limit`, `insufficient_credits`) and Turnstile / rate
  issues surface here as a `failed` event with the matching code — they are **not**
  synchronous errors on `from-url` (except the synchronous `400 turnstile_failed`
  and `429 rate_limited` cases on the initial submit).
</Note>

## SSE Consumer (Node)

A server-side consumer using a streaming `fetch` reader — no extra dependency.
Relay each parsed event to your own browser clients.

```typescript Node / TypeScript theme={null}
async function consumeBuildStream(
  buildId: string,
  onEvent: (e: ProvisioningEvent) => void,
) {
  const res = await fetch(
    `https://app.brainstormer.io/v1/agents/builds/${buildId}/events`,
    {
      headers: {
        Authorization: "Bearer brs_live_YOUR_KEY",
        Accept: "text/event-stream",
      },
    },
  );
  if (!res.ok || !res.body) throw new Error(`stream failed: ${res.status}`);

  const reader = res.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });

    // SSE frames are separated by a blank line.
    const frames = buffer.split("\n\n");
    buffer = frames.pop() ?? "";

    for (const frame of frames) {
      let eventName = "message";
      const dataLines: string[] = [];
      for (const line of frame.split("\n")) {
        if (line.startsWith("event:")) eventName = line.slice(6).trim();
        else if (line.startsWith("data:")) dataLines.push(line.slice(5).trim());
      }
      if (dataLines.length === 0) continue;

      const event: ProvisioningEvent = JSON.parse(dataLines.join("\n"));
      onEvent(event); // relay to your browser clients here

      if (eventName === "done" || eventName === "error") return event;
    }
  }
}

// Usage
const final = await consumeBuildStream("bld_8c9d0e1f2a3b", (e) => {
  console.log(`${e.progress}% — ${e.stepLabel}${e.detail ? ` (${e.detail})` : ""}`);
});
```

Alternatively, with the [`eventsource`](https://www.npmjs.com/package/eventsource)
package (which supports custom headers, unlike the browser built-in):

```typescript Node — eventsource package theme={null}
import { EventSource } from "eventsource";

const es = new EventSource(
  "https://app.brainstormer.io/v1/agents/builds/bld_8c9d0e1f2a3b/events",
  { fetch: (url, init) =>
      fetch(url, { ...init, headers: { ...init.headers, Authorization: "Bearer brs_live_YOUR_KEY" } }) },
);

es.addEventListener("progress", (e) => {
  const event = JSON.parse(e.data);
  console.log(event.progress, event.stepLabel);
});
es.addEventListener("done", (e) => {
  console.log("ready:", JSON.parse(e.data).agentSlug);
  es.close();
});
es.addEventListener("error", (e) => {
  console.error("failed:", JSON.parse((e as MessageEvent).data));
  es.close();
});
```

## curl

```bash curl theme={null}
curl -N https://app.brainstormer.io/v1/agents/builds/bld_8c9d0e1f2a3b/events \
  -H "Authorization: Bearer brs_live_YOUR_KEY" \
  -H "Accept: text/event-stream"
```

## Reconnect

The `id:` of each frame is the event's `emittedAt` timestamp. On reconnect, send
`Last-Event-ID: <last id you saw>` to resume; the stream replays current state on
every connect, so a fresh connect without the header is also safe.

## HTTP Status Codes

| Status                   | Meaning                                            |
| ------------------------ | -------------------------------------------------- |
| `200 OK`                 | Stream opened (`Content-Type: text/event-stream`). |
| `401`                    | Missing or invalid `Authorization` header.         |
| `403 insufficient_scope` | Key lacks the `agents:write` scope.                |
