Developer Docs

StefanBrain Developer API

One clean API surface: API-key auth, chat requests, file attachments, structured outputs, tools, agent runs, and MCP.

Overview

What the API gives you, and which surface to use.

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…UseStart at
Send a message, get finished work backAgent RunsPOST /api/developers/v1/runs
Call one specific capability yourself (web search, Copy Chief review, ad generation, …)ToolsGET /api/developers/v1/tools
Follow a long tool job to completionJobsreturned by async tools
Use StefanBrain inside Claude, Cursor, ChatGPT, or another assistantMCP/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 — these docs as one markdown file. Also give it the OpenAPI spec at /api/developers/v1/openapi. See For agents & tooling.

Quick Start

Send your first request with one curl command.

Quick Start

Create an API key in Settings → Developers. Then start your first run. "sync": true holds the connection until the run finishes:

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):

{
  "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 keys, send them safely, and restrict each key.

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:

Authorization: Bearer stefan_sk_your_key_here

or

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.

Pricing

What your plan includes, per-token rates, and the wallet.

Pricing

Your subscription includes both API surfaces. There are no separate seats, tokens, or add-ons to buy. The two surfaces meter differently:

SurfaceWhat 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)Bills the prepaid API wallet. Your plan adds credit to the wallet every month (see below).

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. There is no model parameter:

Input (per MTok)Output (per MTok)
Agent Runs$1.30$7.80

Agent Runs execute the same model that operates the StefanBrain product.

  • Each token that a request processes bills at these rates. Cached tokens bill as usual input tokens. There is no separate cache price, 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. API calls never use your team's in-app usage. 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:

PlanIncluded API credit
Trial$5 (one-time)
Base$50 / month
Elite$100 / month
Ultra 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

Throughput for each plan, automatic upgrades, and 429 responses.

Rate Limits

Request-count limits change with your plan:

PlanPer key / minutePer account / minutePer day
Trial2040500
Base601202,500
Elite1202405,000
Ultra Pro24048010,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.
  • Each account can process a maximum of 10M tokens each day through the external API. 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 429 response contains your account's effective limits:

{
  "error": {
    "message": "Rate limit exceeded.",
    "type": "rate_limit_error",
    "code": "per_user_tokens_per_day"
  },
  "limits": {
    "per_minute": 120,
    "per_user_per_minute": 240,
    "per_day": 5000,
    "global_tokens_per_day": null,
    "per_user_tokens_per_day": 10000000
  },
  "state": {
    "current": "normal",
    "expires_at": null
  }
}

Polling (run status, run events, job status) does not count against rate limits. Only requests that start work count.

Models

Runs execute the product model. There is no model parameter.

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

Upload files with multipart run requests.

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.
  • File support is the same as regular chat: images, PDFs, common Office documents, text files, and supported transcript-backed audio or video.
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

Get JSON that matches your schema.

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 ["<type>", "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.

{
  "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

Wait for a run in one request, or poll for the result.

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.

Tools

List and call each platform tool directly.

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:

GET  /api/developers/v1/tools
GET  /api/developers/v1/tools/{tool_name}
POST /api/developers/v1/tools/{tool_name}

Each list entry has a kind:

  • sync — the POST returns the result in the response (object: "tool_result"). Examples: web_search, review_copy_with_copy_chief, knowledge_search, knowledge_read, list_projects, get_project_context, amazon_search, google_trends, get_video_transcript, and the managed_connectors_* family.
  • async_submit — the POST starts a background job and returns 202 with object: "tool_job" (see Jobs). Examples: submit_static_ad_generation, submit_video_ad_analysis, submit_create_cuts, submit_angle_finder, submit_cro_funnel_review, research_shortform_content.

Call shape:

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

Tool-level failures (a provider error, an empty scrape) return 200 with is_error: true and diagnostic content. Only transport, auth, validation, and limit problems use error status codes.

Some harness-internal tools (plan, canvas, and turn tools such as todo, create_canvas, export_document) are not served here. They operate only in an agent run. They return 404 tool_not_available with the reason.

Jobs

Poll and control long tool jobs.

Jobs

Async submit tools return a job envelope:

{
  "object": "tool_job",
  "family": "static_ad",
  "job_id": "…",
  "chat": "chat_…",
  "status": "queued",
  "recommended_poll_after_ms": 4000,
  "status_url": "/api/developers/v1/jobs/static_ad/…?chat=chat_…",
  "result_url": "/api/developers/v1/jobs/static_ad/…/result?chat=chat_…",
  "cancel_url": "/api/developers/v1/jobs/static_ad/…/cancel?chat=chat_…"
}

Endpoints (families: static_ad, video_ad, create_cuts, cro_funnel_review, angle_finder, shortform_research, copy_chief):

GET  /api/developers/v1/jobs/{family}/{job_id}
GET  /api/developers/v1/jobs/{family}/{job_id}/result
POST /api/developers/v1/jobs/{family}/{job_id}/cancel

Responses are object: "job" envelopes. The data field carries the family's payload: a status (queued, running, succeeded, failed, or cancelled), a next_action, and per-family result fields. Poll the status until next_action is get_result. Wait a minimum of recommended_poll_after_ms between polls. Then get the result.

A job belongs to the chat where you submitted it. Keep the chat query parameter from the URLs in the submit response. A wrong or missing chat returns 404 job_not_found.

Agent Runs

Run full tool-enabled StefanBrain agents.

Agent Runs

Agent runs give you the full StefanBrain agent over the API. The agent makes a plan, selects tools, runs them, and reports progress. This is the same harness that the product uses.

curl -X POST "$BASE/api/developers/v1/runs" \
  -H "Authorization: Bearer stefan_sk_..." \
  -H "Content-Type: application/json" \
  -d '{"message": "Research trending TikTok hooks for my skincare brand and draft 5 ad angles"}'

Request fields: message (required), chat (an optional chat_... to continue a conversation), project (an optional proj_... id or project UUID), sync (wait for the result, see Sync & Polling), and output_config (a JSON-schema constraint on the final answer, see Structured Outputs). Multipart requests attach files (see Attachments). Runs execute the same model that operates the StefanBrain product (see Models). A run with project executes in that project. The agent gets the project's instructions, file inventory, and retrieved file excerpts, the same as an in-app project chat. Use the list_projects tool to find project ids.

The response is 202 (or 200 with the completed run in sync mode) with object: "agent_run", a run_... id, and the lifecycle URLs:

GET  /api/developers/v1/runs/{run_id}          # status, turn_state, and final output when terminal
GET  /api/developers/v1/runs/{run_id}/events   # progress events; page with ?after={last_event_id}
POST /api/developers/v1/runs/{run_id}/cancel

Poll the status until status is completed, failed, or cancelled. The final assistant text is in output; for structured-output runs the parsed JSON is in structured_output. The events endpoint returns the run's progress stream (turn phases, tool activity, text sections). next_after is the cursor for the next poll. is_done marks the terminal event.

The events endpoint can also stream. Send Accept: text/event-stream and the same events arrive as SSE. Each id: is the event id. Heartbeats come approximately each 15 seconds. The stream closes after the terminal event. To resume at the same position, reconnect with Last-Event-ID or ?after=. JSON cursor polling and SSE carry identical data. Select one for each client.

Runs execute in the background. Your HTTP connection has no effect on them. You can reconnect and poll again at any time.

MCP

Connect Claude, ChatGPT, Cursor, or any MCP client — included in every plan.

MCP

StefanBrain is also a remote MCP server (streamable HTTP):

POST https://stefanbrain.com/api/developers/v1/mcp

claude.ai / Claude Desktop / ChatGPT: add the URL above as a custom connector. The OAuth sign-in flow does the rest (see Supported clients below). All other clients: send Authorization: Bearer stefan_sk_... as the per-client configs below show.

MCP is included in your plan

MCP usage costs nothing extra. Tool calls bill your plan's monthly usage — the same pool as your team's in-app work — never the API wallet. You do not need API credit to use MCP.

Select the surface by who does the work:

  • Use MCP when a person works inside an assistant (Claude, ChatGPT, Cursor) and wants StefanBrain's tools in that conversation.
  • Use the REST API when your code calls StefanBrain: integrations, pipelines, and automated or machine-scale traffic. REST usage bills the API wallet (see Pricing).

The server is stateless. It never issues an Mcp-Session-Id. Each client message is one authenticated POST. A reconnect is always safe. GET (the optional standalone SSE stream) returns 405 by design. There are no server-initiated messages to stream.

Keep the key out of committed config files. Export it one time as STEFANBRAIN_API_KEY. Then use each client's environment interpolation, as the examples below show.

Claude Code

claude mcp add --transport http stefanbrain https://stefanbrain.com/api/developers/v1/mcp \
  --header "Authorization: Bearer stefan_sk_..."

Add --scope user to make the server available in each project, not only the current one.

Cursor

.cursor/mcp.json in your project (or ~/.cursor/mcp.json globally):

{
  "mcpServers": {
    "stefanbrain": {
      "url": "https://stefanbrain.com/api/developers/v1/mcp",
      "headers": { "Authorization": "Bearer ${env:STEFANBRAIN_API_KEY}" }
    }
  }
}

Windsurf

~/.codeium/windsurf/mcp_config.json (note serverUrl, not url):

{
  "mcpServers": {
    "stefanbrain": {
      "serverUrl": "https://stefanbrain.com/api/developers/v1/mcp",
      "headers": { "Authorization": "Bearer ${env:STEFANBRAIN_API_KEY}" }
    }
  }
}

Codex CLI

~/.codex/config.toml:

[mcp_servers.stefanbrain]
url = "https://stefanbrain.com/api/developers/v1/mcp"
bearer_token_env_var = "STEFANBRAIN_API_KEY"

Any streamable-HTTP client

Most other MCP clients accept a JSON block with this shape:

{
  "type": "http",
  "url": "https://stefanbrain.com/api/developers/v1/mcp",
  "headers": { "Authorization": "Bearer stefan_sk_..." }
}

Clients without remote HTTP support (stdio fallback)

Some clients can only launch local stdio servers (for example, older Claude Desktop builds). For these clients, bridge with mcp-remote:

{
  "mcpServers": {
    "stefanbrain": {
      "command": "npx",
      "args": [
        "-y", "mcp-remote",
        "https://stefanbrain.com/api/developers/v1/mcp",
        "--header", "Authorization: Bearer ${STEFANBRAIN_API_KEY}"
      ]
    }
  }
}

Supported clients

Each MCP client connects in one of two ways. Both ways end at the same tool surface.

OAuth (claude.ai, Claude Desktop, ChatGPT, and other connector UIs). StefanBrain operates a full OAuth 2.1 authorization server for the MCP endpoint. "Custom connector" screens work with zero configuration. Paste https://stefanbrain.com/api/developers/v1/mcp as the connector URL. The client finds the sign-in service, registers itself, and sends you to StefanBrain to approve access. Sign in if necessary. Then select Allow access. No API key, no Client ID, no advanced settings. To revoke access, disconnect the connector in the client. This flow is the MCP authorization spec: RFC 9728/8414 discovery, dynamic client registration, and the PKCE authorization-code flow with refresh-token rotation.

API keys (Claude Code, Cursor, Windsurf, Codex CLI, SDKs, scripts). Each client that can send an Authorization: Bearer stefan_sk_... header works with the configs above. Bridge stdio-only clients with mcp-remote.

To approve an OAuth connector, you need the same access as to create an API key: a StefanBrain plan, or the trial. Did you see "Couldn't register with StefanBrain's sign-in service" when you added the connector? That message is older than OAuth support. Remove the connector and add it again.

What you can do

The MCP surface serves the same tools as /api/developers/v1/tools. tools/list and tools/call behave identically. The tool areas:

  • Research & context: knowledge_search (StefanBrain's internal evidence layer; pass queries, then read a hit id with knowledge_read), search_chats (scope: "this_chat" | "other_chats"), read_chat, list_chats, web_search, web_extract, amazon_search, google_trends
  • Projects: list_projects, get_project_context (see below)
  • Copy & creative: review_copy_with_copy_chief, submit_angle_finder, submit_static_ad_generation, submit_video_ad_analysis, submit_create_cuts, submit_cro_funnel_review
  • Shortform: research_shortform_content (use mode: "content" for content research or mode: "creators" for creator sourcing)
  • Connectors: the managed_connectors_* family calls tools on services connected to your account
  • Async jobs: submit_* tools return a family and a run_id. Poll get_job_status. Get the result with get_job_result. Stop the job with cancel_job.

Harness-internal tools (plan, canvas, and turn tools such as todo, create_canvas, export_document) are not served. They operate only in an agent run. tools/call messages count against the standard rate limits. Handshake and tools/list traffic does not count.

Chats and projects

Each tool call runs in a real chat on your account. Job rows and artifacts live in that chat. Two optional headers control the location:

x-stefanbrain-chat: chat_...       # pin to a specific chat
x-stefanbrain-project: proj_...    # pin the session to a project (UUID also accepted)

With no headers, StefanBrain uses your account's "Developer API" chat. With x-stefanbrain-project, the project gets its own "Developer API" chat. Project-aware tools then read the project scope. The most important is get_project_context. Pass query and it retrieves the most relevant excerpts from the project's indexed files. An inaccessible project fails with 404 project_not_found. If the two headers point to a chat in a different project, the call fails with 409 chat_project_mismatch.

Use the project tools to bring a project's context into the client conversation. In-app project chats use the same material:

  • list_projects — the projects your account can access, with proj_... ids.
  • get_project_context — a project's custom instructions, its maintained digest, and the file roster with per-file summaries. Pass query to also get the most relevant excerpts from its indexed files. When the session has the x-stefanbrain-project header, call this tool with no arguments.

A typical flow in Claude Code: list_projectsget_project_context with your question as query → an answer grounded in the project's instructions and files. No copy-paste is necessary.

For Agents & Tooling

Machine-readable docs, OpenAPI, and agent briefs.

For Agents & Tooling

All content on this page is also published in machine-friendly formats. An AI coding agent can build against the API without HTML scraping:

  • /llms-full.txt — these docs as one markdown document. Paste the URL into Claude Code, Cursor, or ChatGPT. Then ask the agent to implement your integration.
  • /llms.txt — a short index of the docs. It follows the llms.txt convention.
  • GET /api/developers/v1/openapi — the OpenAPI 3.1 contract for SDK generators, request validation, and API tooling. No auth is necessary.
  • Copy instructions for AI agents — the button at the top of this page copies a compact implementation brief. Paste the brief into a coding agent.

Does your agent support MCP? Then MCP is the richest integration. The agent gets StefanBrain's tools natively, not through generated HTTP calls. See the MCP section.

Errors

What each non-success response means.

Errors

Each error response uses this envelope:

{
  "error": {
    "message": "Human-readable explanation.",
    "type": "invalid_request_error",
    "code": "invalid_json"
  }
}
  • 400: invalid JSON, a malformed multipart payload, an attachment that is too large, an unsupported tool request, or an invalid structured-output config
  • 401: an API key that is missing, invalid, revoked, or not authorized
  • 403: a suspended API key, account access that cannot use the Developer API, or a key without the scope for this surface (api_key_scope_forbidden)
  • 429: a rate limit (rate_limit_error), an empty API wallet (api_wallet_exhausted), or an empty per-key budget (api_key_budget_exhausted)
  • 500: an unexpected server failure
  • 503: a transient upstream or infrastructure failure

Retry only transient infrastructure failures. Do not retry malformed requests, authorization failures, permission failures, or attachment size errors. Retry after a rate limit only with backoff.

Examples

Reference requests for common flows.

Examples

Structured extraction with a multipart upload:

curl -X POST https://stefanbrain.com/api/developers/v1/runs \
  -H "Authorization: Bearer stefan_sk_your_key_here" \
  -F 'payload={
    "message":"Extract product name, claim set, and CTA recommendations from this PDF.",
    "sync": true,
    "output_config":{
      "format":{
        "type":"json_schema",
        "schema":{
          "type":"object",
          "properties":{
            "productName":{"type":"string"},
            "claims":{"type":"array","items":{"type":"string"}},
            "ctaRecommendations":{"type":"array","items":{"type":"string"}}
          },
          "required":["productName","claims","ctaRecommendations"],
          "additionalProperties":false
        }
      }
    }
  }' \
  -F "files=@/absolute/path/to/brief.pdf"

Best practices:

  • Reuse chat for follow-up turns in a conversation.
  • Store run_id for traceability.
  • Give schemas explicit required fields and additionalProperties: false.