# StefanBrain Developer API > REST and MCP access to StefanBrain, the agentic copywriting platform. Send chat requests, call tools, poll jobs, and run full agents. MCP is included in every plan; REST usage bills per token against a prepaid API wallet that your plan funds with monthly credit. Canonical HTML version: https://dev.stefanbrain.com/docs — OpenAPI 3.1 spec: https://dev.stefanbrain.com/api/developers/v1/openapi # Get started ## Overview The StefanBrain API gives your code the same agent that operates the app: research, hooks, ads, emails, long-form copy, and all the tools. Each StefanBrain plan has API access. The trial also has API access. There is nothing separate to buy: - **MCP is included in your subscription.** MCP usage bills your plan's monthly usage — the same pool as your team's in-app work. It never touches the API wallet. - **The REST API bills a prepaid API wallet.** Your plan adds credit to the wallet every month, automatically. Purchase more credit only when you use the included credit up. See Pricing. Create an API key in **Settings → Developers**. Then select the surface that matches your task: | You want to… | Use | Start at | | --- | --- | --- | | Send a message, get finished work back | **Agent Runs** | `POST /api/developers/v1/runs` | | Call one specific capability yourself (web search, Copy Chief review, ad generation, …) | **Tools** | `GET /api/developers/v1/tools` | | Follow a long tool job to completion | **Jobs** | returned by async tools | | Use StefanBrain inside Claude, Cursor, ChatGPT, or another assistant | **MCP** | `/api/developers/v1/mcp` | Important properties: - Agent Runs are the primary surface. The agent makes a plan, selects tools, does the work, and returns the final text in `output`. Send `"sync": true` to wait for the result in one request, or poll the run URLs. - Runs accept file attachments (multipart) and JSON-schema structured outputs (`output_config`). - The full assistant response always returns in `output`. API turns never write content into in-app canvas documents. - Do you build with an AI coding agent? Give it [/llms-full.txt](/llms-full.txt) — these docs as one markdown file. Also give it the OpenAPI spec at `/api/developers/v1/openapi`. See **For agents & tooling**. ## Quick Start Create an API key in **Settings → Developers**. Then start your first run. `"sync": true` holds the connection until the run finishes: ```bash curl -X POST https://stefanbrain.com/api/developers/v1/runs \ -H "Authorization: Bearer stefan_sk_your_key_here" \ -H "Content-Type: application/json" \ -d '{ "message": "Write three Meta ad hooks for a sleep supplement.", "sync": true }' ``` Response shape (sync mode, completed): ```json { "object": "agent_run", "run_id": "run_550e8400e29b41d4a716446655440000", "chat": "chat_6ba7b8109dad41d180b400c04fd430c8", "status": "completed", "output": "...the finished hooks...", "structured_output": null } ``` When a run needs longer than the sync wait window, the same request returns `202` with the run id and URLs. Poll `GET /api/developers/v1/runs/{run_id}` until `status` is `completed` and read `output`. Keep the `chat` id and send it with your next run to continue the same conversation. ## Authentication Create API keys in **Settings → Developers**. You must accept the current Developer API terms each time you create a key. StefanBrain shows the full secret one time, at creation. Store the secret in a safe location. Send the key in one of these two headers: ```http Authorization: Bearer stefan_sk_your_key_here ``` or ```http x-api-key: stefan_sk_your_key_here ``` An API key has the access of the account that owns it. Invalid, revoked, or expired access returns `401 Unauthorized`. ### Key scopes and budgets When you create a key (`POST /api/developers/keys`), you can restrict it: - `scopes` — an array with values from `runs`, `tools`, `jobs`, `mcp`. A scoped key gets `403 api_key_scope_forbidden` on all other surfaces. Unknown values return `400`. They are not ignored. Omit the field, or send an empty array, for a full-access key. Keys created before scopes existed have full access. The job polling endpoints accept `tools` or `jobs`. A tools-scoped key can always poll the jobs that it submitted. - `monthly_budget_cents` — a spend limit for one key, for one billing cycle. When the key spend is at the limit, requests with that key return `429 api_key_budget_exhausted` until the cycle resets. The account wallet also limits total spend. Omit the field for no key limit. Use scopes and budgets when you give keys to team members or services. Example: a CI key with scope `["tools"]` and a $10 monthly limit. # Billing & limits ## Pricing Your subscription includes both API surfaces. There are no separate seats, tokens, or add-ons to buy. The two surfaces meter differently: | Surface | What it costs | | --- | --- | | **MCP** (Claude, ChatGPT, Cursor, …) | Included in your plan. Usage bills your plan's monthly usage — the same pool as in-app work. The wallet is not touched. | | **REST API** (runs, tools, jobs) | With a `stefan_sk_` key: bills the prepaid **API wallet**. Your plan adds credit to the wallet every month (see below). With an OAuth sign-in (`stefan_oat_`): bills your plan's monthly usage, like MCP. | MCP is for interactive use inside an assistant. Automated, machine-scale traffic must use the REST API. ### REST API rates The wallet bills per token. One rate applies to every request — there is no model parameter: | | Rate (per MTok) | | --- | --- | | Input | $3.50 | | Cached input (cache reads) | $0.35 | | Output | $17.50 | Agent Runs execute the same model that operates the StefanBrain product. - These rates sit 30% below Anthropic's published Claude Opus 4.8 list prices on every line item — input, output, and cached input. - Tokens that repeat across requests in a conversation bill at the cached-input rate automatically. Cache writes bill as ordinary input — there is no write premium, no per-seat fee, and no minimum. - Non-token usage in a request (image generation, web search) bills per use. The exact charge shows in your wallet history. Video generation stays on its own per-minute meter. - The wallet is separate from your plan's app usage. The credential decides the lane: `stefan_sk_` API keys bill only the wallet — never your team's in-app usage — while `stefan_oat_` OAuth sign-ins bill only the plan pool, on REST and MCP alike. App usage never uses the wallet. ### Included monthly credit Your plan adds API credit to the wallet each billing cycle. You do not purchase anything to start — purchase more credit only when you use the included credit up: | Plan | Included API credit | | --- | --- | | Trial | $5 (one-time) | | Base | $50 / month | | Elite | $100 / month | | CA Pro | $500 / month | The monthly credit resets each cycle. The wallet spends the monthly credit first. Top-ups that you purchase (in **Settings → Developers**) stay until spent, for a maximum of 12 months. ### When the wallet is empty Requests return `429` with the code `api_wallet_exhausted`. The response includes a `wallet` object with your balance and the next reset time. Responses that start billable work (run starts, tool calls) also show the live balance in the `x-wallet-remaining-cents` and `x-wallet-resets-at` headers. MCP is not affected — an empty wallet never blocks MCP or in-app use. ## Rate Limits Request-count limits change with your plan: | Plan | Per key / minute | Per account / minute | Per day | | --- | --- | --- | --- | | Trial | 20 | 40 | 2,500 | | Base | 60 | 120 | 10,000 | | Elite | 120 | 240 | 25,000 | | CA Pro | 240 | 480 | 50,000 | - When your active top-up balance is **$100 or more**, your limits move one row down this table automatically. You do not need a plan change to get more throughput. - A separate 10M-tokens-per-day guardrail applies to platform-billed chat requests only; tool, job, run, and MCP traffic never draws on it. Abuse safeguards also apply. Normal sustained integration traffic does not activate them. - Do you need more? Contact us. Per-account overrides are routine for production integrations. Spend limits and throughput limits are separate. The API wallet controls REST spend (`429 api_wallet_exhausted`, see Pricing). Your plan's monthly pool controls interactive MCP usage (`429 monthly_usage_limit_reached`). Each rate-limit `429` carries a `Retry-After` header (seconds), and the body names the binding limit, your observed counts, and the reset time: ```json { "error": { "message": "Daily request limit reached (2500/2500 requests today on the Trial plan). Resets at 2026-08-15T00:00:00.000Z. Job status/result polling never counts against rate limits.", "type": "rate_limit_error", "code": "request_per_day" }, "limits": { "per_minute": 20, "per_user_per_minute": 40, "per_day": 2500, "global_tokens_per_day": null, "per_user_tokens_per_day": 10000000 }, "observed": { "minute_count": 3, "user_minute_count": 3, "day_count": 2501 }, "plan": "trial", "reset_at": "2026-08-15T00:00:00.000Z", "state": { "current": "normal", "expires_at": null } } ``` Polling (run status, run events, job status) does not count against rate limits — on REST **and** on MCP, where `get_job_status`, `get_job_result`, and `cancel_job` calls are free. Only requests that start work count. # Runs in depth ## Models There is no `model` request field. You do not select a model. If a request includes a `model` field, StefanBrain ignores it. Agent Runs (`POST /v1/runs`) execute the same model that operates the StefanBrain product. When the product model changes, runs change with it — your integration stays current without code changes. ## Attachments Use `multipart/form-data` on `POST /v1/runs` when StefanBrain must read files with the message. - Put the JSON request body in a `payload` field. - Add each upload as a `files` field. - The maximum is 10 files for each run. Size limits match in-app chat uploads. - Supported types: images, PDFs, common Office documents, spreadsheets, and text files. Video and audio files are not accepted — the run can still read video links, so host the file and put its URL in the run's `message`. ```bash curl -X POST https://stefanbrain.com/api/developers/v1/runs \ -H "Authorization: Bearer stefan_sk_your_key_here" \ -F 'payload={ "message":"Summarize the attached deck and give me three CTA options.", "sync": true }' \ -F "files=@/absolute/path/to/deck.pdf" ``` Use uploaded `files` only. Internal attachment ids and referenced document ids are not part of the public API. ## Structured Outputs Add `output_config.format` to a run request and the run's final answer is constrained to your JSON schema. The raw text returns in `output`; the parsed JSON returns in `structured_output`. Structured outputs run in strict mode. Strict mode has three rules beyond ordinary JSON Schema: - The root schema must have `"type": "object"`. - Every object must set `"additionalProperties": false`. - Every object must list ALL of its `properties` keys in `required`. Mark a field optional with a `["", "null"]` type union instead. A schema that breaks a rule returns `400` with the code `invalid_output_config` and a message that names the exact path. ```json { "message": "Extract the offer details from this landing page summary.", "sync": true, "output_config": { "format": { "type": "json_schema", "schema": { "type": "object", "properties": { "headline": { "type": "string" }, "audience": { "type": "string" }, "benefits": { "type": "array", "items": { "type": "string" } } }, "required": ["headline", "audience", "benefits"], "additionalProperties": false } } } } ``` `structured_output` is `null` when the run is not completed, or when the final text is not valid JSON (a refusal, or a hard failure). The agent can still use tools during a structured-output run; the schema constrains the final answer only. ## Sync & Polling Runs execute in the background. Your HTTP connection has no effect on them. Two ways to get the result: **Sync mode.** Add `"sync": true` to the run request. The connection holds until the run finishes and the response is the completed run (`200`) with `output` and `structured_output`. When the run needs longer than the sync wait window (about 90 seconds), the response is the normal `202` envelope instead — no work is lost; finish by polling. **Polling.** The `202` envelope carries the run id and URLs. Poll `GET /api/developers/v1/runs/{run_id}` one time each 3–10 seconds until `status` is `completed`, `failed`, or `cancelled`. Polling does not count against rate limits. Only requests that start work count. Use sync mode for interactive requests and short tasks. Use polling for long agentic work, batch pipelines, and any place a held HTTP connection is fragile. ### Busy chats: on_busy A chat runs one turn at a time. Starting a run (or an `ask_stefanbrain` call) into a chat that already has an active run returns a `409` busy error by default — no wait-and-retry loop is needed, because both surfaces take an optional `on_busy` field: - `"reject"` (default): the `409` busy error, exactly as before. - `"queue"`: the run is accepted and queued behind the chat's active run. The `202` envelope (or the ask's parked envelope) is the normal one — `run_id`, `status: "queued"` — plus `queued_behind_run_id`, the run it waits on. It starts automatically when the chat frees up; multiple queued runs on one chat start in creation order (FIFO). Poll exactly as usual: `status` stays `queued` until the run starts. Queueing is per-chat: it applies to whatever chat the request addressed — the connector session chat, a `session`-labeled chat, a project's session chat, or an explicit `chat` id. A queued run that has not started within 30 minutes fails with `status: "failed"` and a `last_error` starting with `queue_timeout:` — retry it, or start it in a fresh chat. Cancelling a queued run works like cancelling any other run. A queued run holds one of your plan's concurrent-agent slots while it waits. Stacking queued asks past that limit returns `429 agentcore_user_concurrency_reached` instead of queueing deeper, so queue depth is bounded by your concurrency limit. # Tools & agents ## Tools Your code can call each tool that StefanBrain's own agents use. Get the live catalog (names, JSON Schemas, sync or async shape). Then call a tool by name: ```http GET /api/developers/v1/tools GET /api/developers/v1/tools/{tool_name} POST /api/developers/v1/tools/{tool_name} ``` Each catalog entry carries the tool's `input_schema` and its `output_schema` — the result contract for `structured_content` (`null` for tools without a declared one). These are the same JSON Schemas the MCP surface advertises; generate types from them instead of hand-writing result shapes. Each list entry has a `kind`: - `sync` — the POST returns the result in the response (`object: "tool_result"`). Examples: `web_search`, `list_projects`, `get_project_context`, `amazon_search`, `google_trends`, and the `managed_connectors_*` family. - `async_submit` — the POST starts a background job and returns `202` with `object: "tool_job"` (see Jobs). Examples: `create_images`, `find_angles`, `review_funnel`, `research_shortform`. Call shape: ```bash curl -X POST "$BASE/api/developers/v1/tools/web_search" \ -H "Authorization: Bearer stefan_sk_..." \ -H "Content-Type: application/json" \ -d '{"arguments": {"query": "best hook formats for supplement ads"}}' ``` Request fields: - `arguments` — an object that matches the tool's `input_schema`. Schema violations return `400 invalid_tool_arguments`. - `Idempotency-Key` (header) — safe blind retries for submit tools and `ask_stefanbrain`. Resubmitting the same key within a 24-hour window (UTC day) returns the ORIGINAL job envelope — the same `job_id`, no new work, no second charge — so retry a timed-out submit with the same key instead of guessing whether it landed. The header value becomes the tool's `idempotency_key` argument (MCP callers pass that argument directly); other tools ignore it. Keys are 1–128 characters of `A-Z a-z 0-9 _ . : -` (`400 invalid_idempotency_key` otherwise), scoped to your account and the chat the call resolves. Because the chat is part of that scope, a keyed retry must carry the same `session`/`project` arguments — a different or missing label resolves a different chat and starts new work. Exceptions: `ask_stefanbrain` dedupes for at least 6 hours after the turn finishes, within the same UTC day — its dedupe rides the run record, which is purged 6 hours after completion, so it is narrower than the submit doors' full 24 hours; `research_shortform` is not idempotent (documented boundary — mint a fresh request or check your chat before retrying); and `review_copy` also keys on the request body, so the same key with different arguments starts a new job. - `chat` — an optional `chat_...` reference. Tool activity and artifacts attach to a chat that you own. Without `chat`, StefanBrain uses your account's "Developer API" chat, or creates it. The response always contains the chat that was used. - `project` — an optional `proj_...` id (from `list_projects`) or project UUID. The call runs in that project. Without `chat`, the project gets its own "Developer API" chat. Project-aware tools, for example `get_project_context`, then read the project's files. An unknown or inaccessible project returns `404 project_not_found`. A `chat` that belongs to a different project returns `409 chat_project_mismatch`. The response contains the effective project. - `arguments.project` / `arguments.session` — `ask_stefanbrain` and the `submit_*` tools also take addressing INSIDE `arguments` (the same params their MCP schemas advertise). `project` there beats the body `project` field. `session` is a label (1–128 characters of `A-Z a-z 0-9 _ . : -`; `400 invalid_session` otherwise) that gives the call its own chat, "Developer API —