> ## 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 Auth & Rate Limits

> API key scopes (agents:write, chat), the no-secrets-in-browser rule, Cloudflare Turnstile, 429 / Retry-After rate limits, and idempotency for the Agent Provisioning API.

# Authentication & Rate Limits

The Agent Provisioning API has **two trust zones**:

| Surface                                                             | Auth                         | Who calls it             |
| ------------------------------------------------------------------- | ---------------------------- | ------------------------ |
| `POST /v1/agents:from-url`, `GET /v1/agents/builds/:id`, `…/events` | `brs_live_` API key          | **Your backend only**    |
| `POST /api/public/agents/:slug/*` (chat)                            | None (anonymous, slug-based) | **The browser directly** |

## API keys & scopes

`brs_live_` keys are **organization-scoped** — the consumer brings its own
Brainstormer org + key, and all provisioned agents and their cost bill to that
org. Pass the key as a bearer token:

```
Authorization: Bearer brs_live_YOUR_KEY
```

Keys carry **scopes**. The provisioning endpoints require **`agents:write`**.
A key missing the required scope is rejected by the gateway:

| Scope          | Grants                                                             |
| -------------- | ------------------------------------------------------------------ |
| `agents:write` | Provision agents (`/v1/agents:from-url` + build status endpoints). |
| `chat`         | Chat with agents (`/v1/chat`).                                     |
| `kb:read`      | Search knowledge bases (`/v1/kb/search`).                          |

```json 403 — missing scope theme={null}
{
  "error": "insufficient_scope",
  "message": "This API key is missing the required \"agents:write\" scope."
}
```

Select a key's scopes when you generate it in the **Developer Portal**
(`/developer` → pick an application → **Generate Key**). New keys default to
`chat` and `kb:read`; tick **`agents:write`** to allow provisioning. A key's
granted scopes are listed alongside it in the key table.

## No secrets in the browser (READ FIRST)

<Warning>
  A **browser must never hold a `brs_live_` key.** Provisioning is a cost-incurring
  write (scrape + LLM + embeddings), so it is authorized by a secret your server
  controls — never shipped to end users.
</Warning>

The only supported v1 integration is **Pattern A — client backend proxy**:

```
Browser ──▶ Your backend ──(brs_live_ key)──▶ POST /v1/agents:from-url
Browser ◀── Your backend ◀── SSE relay / webhook
Browser ──────────────────────────────────▶ POST /api/public/agents/:slug/chat  (direct, no key)
```

The asymmetry that makes this safe: **chat never needs a secret** — it is the
anonymous, slug-based surface, embeddable front-end-only. Only the *provisioning*
write is gated, and that's the part your backend handles. See the
[quickstart](/developer/provisioning-quickstart#pattern-a-backend-proxy) for a
Next.js reference.

<Note>
  A publishable, origin-scoped browser session token (**Pattern B**) for
  backend-less consumers is **deferred — not in v1**.
</Note>

## Cloudflare Turnstile (bot protection)

`from-url` accepts a Cloudflare Turnstile token, verified server-side. When the
platform is configured with a `TURNSTILE_SECRET_KEY`, the token is **required**.

1. Render the Turnstile widget in the browser; obtain a token per submit.
2. Relay the token to your backend.
3. Include it as `turnstileToken` on the `from-url` request body.

A missing or invalid token returns **`400 turnstile_failed`** synchronously. (A
later bot-check rejection inside the build surfaces as a `failed` event with
error code `turnstile_failed`.)

## Rate limits — `429` + `Retry-After`

`from-url` is rate-limited on **two** axes:

| Limit   | Default                            | Scope                        |
| ------- | ---------------------------------- | ---------------------------- |
| Per-key | `api_keys.rate_limit_rpm` (60 rpm) | Your API key                 |
| Per-IP  | 10 requests / minute               | The submitting IP (stricter) |

Over-limit requests return **`429`** with a **`Retry-After`** header (in
**seconds**). Back off and retry after that delay.

```http theme={null}
HTTP/1.1 429 Too Many Requests
Retry-After: 30
```

```typescript Backend — honor Retry-After theme={null}
async function submitWithBackoff(body: object, attempt = 0): Promise<Response> {
  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",
    },
    body: JSON.stringify(body),
  });
  if (res.status === 429 && attempt < 3) {
    const wait = Number(res.headers.get("Retry-After") ?? 30) * 1000;
    await new Promise((r) => setTimeout(r, wait));
    return submitWithBackoff(body, attempt + 1);
  }
  return res;
}
```

Rate limits hit *inside* a running build (rather than on submit) surface as a
`failed` event with error code `rate_limited` (`retryable: true`).

## Idempotency

Send an `Idempotency-Key` header (a UUID you generate) on `from-url` so a
double-submit — a retried network request, a double-clicked button — returns the
**same** build instead of spawning a second one.

```
Idempotency-Key: 7f1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d
```

Combined with **handle dedup** (a fresh existing published agent for the same
normalized handle is reused and returned `ready` instantly), repeat submits are
safe, fast, and free. See [Build lifecycle](/developer/provisioning-concepts#dedup--idempotency).

## Error reference

| HTTP  | Code                 | When                                            |
| ----- | -------------------- | ----------------------------------------------- |
| `400` | `unsupported_url`    | URL isn't a readable YouTube/Instagram channel. |
| `400` | `turnstile_failed`   | Missing/invalid Turnstile token.                |
| `401` | —                    | Missing or invalid `Authorization` header.      |
| `403` | `insufficient_scope` | Key lacks `agents:write`.                       |
| `429` | `rate_limited`       | Per-key or per-IP limit; honor `Retry-After`.   |

For the in-build error model (`plan_limit`, `insufficient_credits`,
`scrape_failed`, …) with `retryable` semantics, see the
[ProvisioningErrorCode table](/api-reference/provisioning/build-events-sse#provisioningerrorcode-table).
