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

# Provisioning Quickstart

> Build a chat agent from a URL and embed chat in 5 minutes — create an org + API key, mint the agents:write scope, call from-url, consume the SSE stream, and chat.

# Build an Agent from a URL in 5 Minutes

This quickstart takes you from zero to an embedded, grounded chat agent built
from a creator's YouTube or Instagram channel. The flow:

<Steps>
  <Step title="Create an org + API key">
    Get a `brs_live_` key with the `agents:write` scope.
  </Step>

  <Step title="Provision from a URL">
    Your backend `POST`s to `/v1/agents:from-url` → `202` with a `buildId`.
  </Step>

  <Step title="Stream progress">
    Your backend consumes the SSE stream and relays progress to the browser.
  </Step>

  <Step title="Chat">
    On `ready`, the browser chats directly with the agent's public slug.
  </Step>
</Steps>

<Warning>
  The `brs_live_` key is a **server-side secret**. NEVER ship it to a browser. The
  recommended (and only v1) integration is **Pattern A** below: your backend holds
  the key and relays events; the browser talks to your backend, then chats
  directly with the anonymous slug endpoints.
</Warning>

## 1. Create an org + API key

API keys are **organization-scoped** — all provisioned agents and their cost
bill to that org. Create (or pick) a Brainstormer org, then mint an API key with
the **`agents:write`** scope (chat needs `chat`, which is a default scope).

Set the key in your backend environment — never in client code:

```bash theme={null}
export BRAINSTORMER_API_KEY="brs_live_YOUR_KEY"
```

See [Authentication & rate limits](/developer/provisioning-auth) for scopes,
Turnstile, rate limits, and idempotency.

## 2. Provision from a URL

Your backend submits the build. The request returns immediately (`202`) — it does
not block while the agent is built.

```typescript Backend — submit build theme={null}
import { randomUUID } from "node:crypto";

const res = await fetch("https://app.brainstormer.io/v1/agents:from-url", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.BRAINSTORMER_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": randomUUID(),
  },
  body: JSON.stringify({
    url: "https://youtube.com/@channel",
    turnstileToken,           // relayed from the browser
    mode: "preview",          // bounded demo ingestion
    intent: "creator",
  }),
});

const build = await res.json();
// { buildId, status, streamUrl, pollUrl, agentSlug? }
```

If `status` is already `ready` (a handle-dedup cache hit), skip straight to
[step 4](#4-chat-from-the-browser) with `build.agentSlug`.

## 3. Stream progress (and relay to the browser)

The SSE stream requires the `Authorization` header, so it **must** be consumed
server-side (the browser's `EventSource` can't send headers). Your backend reads
the stream and relays each event to the browser over your own SSE/WebSocket.

```typescript Backend — consume + relay theme={null}
async function consume(buildId: string, relay: (e: any) => void) {
  const res = await fetch(
    `https://app.brainstormer.io/v1/agents/builds/${buildId}/events`,
    {
      headers: {
        Authorization: `Bearer ${process.env.BRAINSTORMER_API_KEY}`,
        Accept: "text/event-stream",
      },
    },
  );
  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 });
    const frames = buffer.split("\n\n");
    buffer = frames.pop() ?? "";
    for (const frame of frames) {
      let name = "message";
      const data: string[] = [];
      for (const line of frame.split("\n")) {
        if (line.startsWith("event:")) name = line.slice(6).trim();
        else if (line.startsWith("data:")) data.push(line.slice(5).trim());
      }
      if (!data.length) continue;
      const event = JSON.parse(data.join("\n"));
      relay(event);                       // forward to your browser clients
      if (name === "done" || name === "error") return event;
    }
  }
}
```

See the full [SSE reference](/api-reference/provisioning/build-events-sse) for
the wire format, the `eventsource`-package variant, and reconnect via
`Last-Event-ID`.

## Pattern A: backend proxy (Next.js)

A complete reference: a Next.js Route Handler that holds the key and **streams a
relay** to the browser. The browser opens an `EventSource` against *your* route,
never against Brainstormer.

```typescript app/api/build/route.ts (Next.js Route Handler) theme={null}
import { randomUUID } from "node:crypto";

export const runtime = "nodejs";

export async function POST(req: Request) {
  const { url, turnstileToken } = await req.json();

  // 1. Submit the build with the secret key (server-side only).
  const submit = await fetch("https://app.brainstormer.io/v1/agents:from-url", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.BRAINSTORMER_API_KEY!}`,
      "Content-Type": "application/json",
      "Idempotency-Key": randomUUID(),
    },
    body: JSON.stringify({ url, turnstileToken, mode: "preview", intent: "creator" }),
  });
  const build = await submit.json();

  // 2. Open the upstream SSE stream (server-side, with the key) and relay it
  //    to the browser as a fresh SSE response — the key never leaves the server.
  const upstream = await fetch(
    `https://app.brainstormer.io${build.streamUrl}`,
    {
      headers: {
        Authorization: `Bearer ${process.env.BRAINSTORMER_API_KEY!}`,
        Accept: "text/event-stream",
      },
    },
  );

  return new Response(upstream.body, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache, no-transform",
      Connection: "keep-alive",
    },
  });
}
```

```typescript Browser — talk only to YOUR backend theme={null}
// Kick off the build via your own route; consume the relayed stream.
const resp = await fetch("/api/build", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ url, turnstileToken }),
});

const reader = resp.body!.getReader();
const decoder = new TextDecoder();
let agentSlug: string | undefined;
let buffer = "";
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  buffer += decoder.decode(value, { stream: true });
  const frames = buffer.split("\n\n");
  buffer = frames.pop() ?? "";
  for (const frame of frames) {
    const dataLine = frame.split("\n").find((l) => l.startsWith("data:"));
    if (!dataLine) continue;
    const event = JSON.parse(dataLine.slice(5).trim());
    updateProgressBar(event.progress, event.stepLabel); // your UI
    if (event.status === "ready") agentSlug = event.agentSlug;
  }
}
```

<Tip>
  For an "email-me-a-link" UX where you don't hold a connection, set
  `deliver.webhookUrl` and verify the
  [HMAC-signed webhook](/api-reference/provisioning/webhook) instead of streaming —
  or poll the [snapshot endpoint](/api-reference/provisioning/get-build).
</Tip>

## 4. Chat from the browser

Once you have `agentSlug`, the browser chats **directly** — no key, no proxy.
Generate a UUID `anonymousId` per visitor and seed the UI with the
`starterQuestions` from the `ready` event.

```typescript Browser — chat theme={null}
const anonymousId =
  localStorage.getItem("bsio_anon") ??
  (localStorage.setItem("bsio_anon", crypto.randomUUID()),
  localStorage.getItem("bsio_anon")!);

// Create a conversation
const conv = await fetch(
  `https://app.brainstormer.io/api/public/agents/${agentSlug}/conversations`,
  {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ anonymousId }),
  },
).then((r) => r.json());

const conversationId = conv.data.conversationId;

// Send a message (streaming)
const chat = await fetch(
  `https://app.brainstormer.io/api/public/agents/${agentSlug}/chat`,
  {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      message: "What videos have you made about productivity?",
      conversationId,
      anonymousId,
      stream: true,
    }),
  },
);
// read chat.body for the token stream — see the Anonymous Chat reference
```

See the full [Anonymous Chat reference](/api-reference/provisioning/anonymous-chat).

<Tip>
  If the agent has file uploads enabled, visitors can attach files: upload via
  [`POST /api/public/agents/:slug/attachments`](/api-reference/provisioning/anonymous-chat#upload-attachment)
  and pass the returned `{ id, accessToken }` in the message's `attachments` array.
</Tip>

## Next steps

<CardGroup cols={2}>
  <Card title="Authentication & rate limits" icon="key" href="/developer/provisioning-auth">
    Scopes, Turnstile, 429 / Retry-After, idempotency.
  </Card>

  <Card title="Build lifecycle" icon="diagram-project" href="/developer/provisioning-concepts">
    The FSM, preview vs. full ingestion, dedup, and preview TTL.
  </Card>

  <Card title="from-url reference" icon="play" href="/api-reference/provisioning/from-url">
    Every request/response field and status code.
  </Card>

  <Card title="SSE reference" icon="wave-sine" href="/api-reference/provisioning/build-events-sse">
    Wire format, ProvisioningEvent, error codes.
  </Card>
</CardGroup>
