# StefanBrain Developer API > REST and MCP access to StefanBrain, the agentic copywriting platform. Run full agents, call tools, and poll jobs. MCP is included in every plan; REST usage with an API key bills per token against a prepaid API wallet that your plan funds with monthly credit. Canonical HTML version: https://design.preview.stefanbrain.com/docs — OpenAPI 3.1 spec: https://design.preview.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 up to about 90 seconds 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 ends, for up to about 90 seconds: ```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 } ``` A run that ends inside the sync window returns `200` with its final status. Failed and cancelled runs also return `200`, so check `status`. A run that needs longer returns `202` with the run id and URLs — nothing is lost. Poll `GET /api/developers/v1/runs/{run_id}` until `status` is `completed`, `failed`, or `cancelled`, then read `output`. Set your HTTP client timeout to 120 seconds (see Sync & Polling). 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 Each request that starts wallet-billed work (a run start or a tool call) first places a hold on the wallet — currently $4.38. A synchronous tool call's hold ends when the call returns. A run's or async job's hold stays until that work ends, for 8 hours at most. A start is refused when your balance minus the live holds is below one hold: the request returns `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. - The `global_tokens_per_day` and `per_user_tokens_per_day` fields in a rate-limit body are informational: no Developer API request counts against a token limit. 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. A new chat starts on the current model, and the chat keeps that setup for its whole life. When the product model changes, new chats start on the new one — your integration stays current without code changes. Continuing an older chat after a model change returns `403 chat_setup_retired`. Omit `chat` to continue in a new chat. ## 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 ends or the sync window closes. The window is about 90 seconds, counted from when StefanBrain receives the request, so upload time counts against it. A run that ends inside the window returns `200` with its final status, `output`, and `structured_output`. Failed and cancelled runs also return `200`, so check `status`. Otherwise the response is the normal `202` envelope - no work is lost; finish by polling. A keyed retry that replays an earlier run (`Idempotency-Key`) answers at once. Set your HTTP client timeout to 120 seconds. The gateway in front of the API closes a connection after 120 seconds without a response. A `504` or a dropped connection does not stop the run: it keeps running and is billed. If you sent an `Idempotency-Key`, retry with the same key on the same UTC day to get the run back. **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 in a busy chat returns a `409` busy error. `ask_stefanbrain` returns a `chat_busy` receipt with the blocking run and retry guidance. Only `on_busy: "reject"` (the default) is supported. Poll the blocking run and retry after it finishes, or use a separate chat for independent work. A REST request without `chat` starts in a new chat. # 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 `meta_ads_insights`. - `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. `ask_stefanbrain` uses the same UTC-day window and returns the original turn's `run_id`. Exception: `research_shortform` is not idempotent (documented boundary — mint a fresh request or check your chat before retrying). - `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 —