> ## Agent Instructions > Follow the content contract in AGENTS.md. > Treat text as the canonical explanation; video and screenshots enhance it. > Do not publish unverified product behavior or duplicate an existing canonical article. # List your feedback reports and the team's replies Source: https://docs.stefanbrain.com/api-reference/feedback/list-your-feedback-reports-and-the-teams-replies /developers/openapi/stefanbrain-v1.json get /api/developers/v1/feedback The reports this account sent, newest first, each with its status and the team's reply: open (no reply yet), acknowledged (seen, being worked on), resolved (fixed; the reply says what changed), wont_fix (the reply says why). Free, never rate-limited. # Read one feedback report and the team's reply Source: https://docs.stefanbrain.com/api-reference/feedback/read-one-feedback-report-and-the-teams-reply /developers/openapi/stefanbrain-v1.json get /api/developers/v1/feedback/{feedbackId} One report this account sent. Poll it to learn when the team replies. Free, never rate-limited. # Tell StefanBrain where the API got in your way Source: https://docs.stefanbrain.com/api-reference/feedback/tell-stefanbrain-where-the-api-got-in-your-way /developers/openapi/stefanbrain-v1.json post /api/developers/v1/feedback For agents first: when an endpoint or MCP tool fails, returns the wrong thing, answers with an error that does not say how to fix it, or lacks something you need, send one report per problem while you still have the request, the error code and the id. The StefanBrain team reads every report and replies; read the reply with GET /v1/feedback/{feedbackId}. Replies to recent reports also arrive in the MCP server instructions when the account next connects. Free (never billed), 60 reports an hour per account, any key scope. Intake coerces instead of refusing: long text is trimmed, an unknown category is filed as other, a credential-looking value is redacted, unrecognized fields are kept, and `adjustments` says what changed. Only a report with no text is refused. # Download a workspace file's current head bytes Source: https://docs.stefanbrain.com/api-reference/files/download-a-workspace-files-current-head-bytes /developers/openapi/stefanbrain-v1.json get /api/developers/v1/files/{fileId}/download Use the listing entry's download_url verbatim: it already carries the file id, chat_id, and path. # List a chat's current workspace file heads Source: https://docs.stefanbrain.com/api-reference/files/list-a-chats-current-workspace-file-heads /developers/openapi/stefanbrain-v1.json get /api/developers/v1/files Complete snapshot of the chat's current workspace file heads: one entry per path, no pagination (a cursor parameter is a 400) and no file history. Uploaded input files are not listed. # Upload local files for ask_stefanbrain to see Source: https://docs.stefanbrain.com/api-reference/files/upload-local-files-for-ask_stefanbrain-to-see /developers/openapi/stefanbrain-v1.json post /api/developers/v1/files Pre-stage up to 10 files from your machine (images, PDFs, docs, spreadsheets — anything the app's chat accepts except video/audio) so an MCP `ask_stefanbrain` call can attach them via `file_ids`. Send multipart/form-data with repeated `files` fields, e.g. `curl -F "files=@ad.png" -F "files=@brief.pdf"`. Uploads expire after 24 hours and can be reused across asks. Rate-limited but never metered. # Cancel a job Source: https://docs.stefanbrain.com/api-reference/jobs/cancel-a-job /developers/openapi/stefanbrain-v1.json post /api/developers/v1/jobs/{family}/{runId}/cancel # Download a job artifact's bytes (static_ad creative images) Source: https://docs.stefanbrain.com/api-reference/jobs/download-a-job-artifacts-bytes-static_ad-creative-images /developers/openapi/stefanbrain-v1.json get /api/developers/v1/jobs/{family}/{runId}/artifacts/{artifactId} Streams one stored static_ad creative image. Job results do not list artifacts: take the creative id from `creatives[].id` in the ad.json file the job writes to its chat workspace (see the files endpoints), and pass the chat the job was submitted under. Other families return 404 artifact_not_found. Never metered. # Fetch a completed job's result Source: https://docs.stefanbrain.com/api-reference/jobs/fetch-a-completed-jobs-result /developers/openapi/stefanbrain-v1.json get /api/developers/v1/jobs/{family}/{runId}/result # Get job status Source: https://docs.stefanbrain.com/api-reference/jobs/get-job-status /developers/openapi/stefanbrain-v1.json get /api/developers/v1/jobs/{family}/{runId} # Read a stored Meta video transcript or ask for one Source: https://docs.stefanbrain.com/api-reference/media/read-a-stored-meta-video-transcript-or-ask-for-one /developers/openapi/stefanbrain-v1.json post /api/developers/v1/media/transcripts/request One transcript per video, stored once by content hash and shared by every account with access to an ad account that holds the video. Answers `ready` from the store, `queued` with the one job producing it (repeat the same request to poll), or `unavailable` with the reason. Rate-limited, never metered. Requires the `tools` scope. # Read the stored transcript of a Meta video by content hash Source: https://docs.stefanbrain.com/api-reference/media/read-the-stored-transcript-of-a-meta-video-by-content-hash /developers/openapi/stefanbrain-v1.json get /api/developers/v1/media/transcripts/{contentSha256} Never transcribes. 404 transcript_not_found until a request has stored the row; a stored failure is served as `unavailable` with its reason. # Cancel a run Source: https://docs.stefanbrain.com/api-reference/runs/cancel-a-run /developers/openapi/stefanbrain-v1.json post /api/developers/v1/runs/{runId}/cancel # Get run status Source: https://docs.stefanbrain.com/api-reference/runs/get-run-status /developers/openapi/stefanbrain-v1.json get /api/developers/v1/runs/{runId} # List run events (poll with the cursor parameter, or stream them as SSE) Source: https://docs.stefanbrain.com/api-reference/runs/list-run-events-poll-with-the-cursor-parameter-or-stream-them-as-sse /developers/openapi/stefanbrain-v1.json get /api/developers/v1/runs/{runId}/events Poll with ?after= for JSON pages, or send Accept: text/event-stream to receive the same events as Server-Sent Events: each `id:` is the event id, and the stream closes after the terminal event. Resume a stream with the Last-Event-ID header or ?after=. # Start an agent run Source: https://docs.stefanbrain.com/api-reference/runs/start-an-agent-run /developers/openapi/stefanbrain-v1.json post /api/developers/v1/runs # Describe one tool (input schema, cost class) Source: https://docs.stefanbrain.com/api-reference/tools/describe-one-tool-input-schema-cost-class /developers/openapi/stefanbrain-v1.json get /api/developers/v1/tools/{toolName} # Invoke a tool synchronously (or enqueue a job for job-backed tools) Source: https://docs.stefanbrain.com/api-reference/tools/invoke-a-tool-synchronously-or-enqueue-a-job-for-job-backed-tools /developers/openapi/stefanbrain-v1.json post /api/developers/v1/tools/{toolName} # List invocable tools Source: https://docs.stefanbrain.com/api-reference/tools/list-invocable-tools /developers/openapi/stefanbrain-v1.json get /api/developers/v1/tools # Agent Runs Source: https://docs.stefanbrain.com/developers/agent-runs Start full StefanBrain agent runs, attach files, request structured output, and follow progress to completion. Agent Runs give your application the full StefanBrain agent. The agent plans the work, selects tools, executes them, and returns the finished response. ## Start a run ```bash theme={"system"} 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": "Research trending TikTok hooks for my skincare brand and draft five ad angles." }' ``` The request accepts these fields: | Field | Description | | - | - | | `message` | Required instructions for the run. | | `chat` | Optional `chat_...` identifier (or chat UUID) used to continue a conversation. Omit it to start a new chat. | | `project` | Optional `proj_...` identifier or project UUID. | | `sync` | Wait for the finished response when possible. | | `on_busy` | Only `reject` (the default) is supported. Busy chats are refused. | | `output_config` | Constrain the final response with a JSON schema. | A run with `project` addresses a chat inside that project. Project instructions and summaries are not added to the run automatically. Use the `list_projects` tool to find project identifiers. ## Understand the response Starting a run normally returns `202` with the run handle: `run_id`, `chat`, `project` (when the chat is in a project), `status: "queued"`, `status_url`, `events_url`, and `cancel_url`. A synchronous run that ends inside the wait window returns `200` with the run's status body instead: `run_id`, `chat`, `status`, `output`, `structured_output`, `last_error`, `cancel_requested`, and timestamps. The `200` body has no URLs; the lifecycle paths are: ```http theme={"system"} GET /api/developers/v1/runs/{run_id} GET /api/developers/v1/runs/{run_id}/events POST /api/developers/v1/runs/{run_id}/cancel ``` Poll the status until it is `completed`, `failed`, or `cancelled`. * `output` holds the latest assistant text. It is the final answer only once `status` is `completed`. * `structured_output` contains parsed JSON when the run requested a structured response. * `last_error` provides failure detail when the run fails. Runs execute in the background. Closing the HTTP connection does not cancel the work; you can reconnect and poll again. `POST .../cancel` returns the run's status with `cancel_requested: true`. A running run stops and reports `cancelled` shortly after. An admitted run that has not started can also be cancelled. Cancellation does not count against rate limits. ## Control busy-chat behavior A chat executes one run at a time. Only `on_busy: "reject"` (the default) is supported. A busy start returns `409` with the blocking run id when known. Poll the blocking run and retry after it finishes. Use a separate chat for independent parallel work. ## Make a retry idempotent Add an `Idempotency-Key` header when you may need to retry a start without knowing whether the first request succeeded: ```bash theme={"system"} curl -X POST https://stefanbrain.com/api/developers/v1/runs \ -H "Authorization: Bearer stefan_sk_your_key_here" \ -H "Idempotency-Key: research-brief-2026-08-27" \ -H "Content-Type: application/json" \ -d '{"message":"Build a positioning brief from this category research."}' ``` A repeated key returns the original run: the `202` envelope while it is queued or running, or its final status (`200`) once it ends. It does not create another chat, stage attachments again, or start another billed run. The retry's body is not compared with the original. An invalid key returns `400 invalid_idempotency_key`. Send a key with every run start. A timeout, dropped connection, or gateway `504` does not stop a run, so a lost response is otherwise a second billed run on retry. Run keys are scoped to your account and remain effective for the rest of the same UTC day. Valid keys contain 1–128 URL-safe characters: letters, numbers, `_`, `.`, `:`, or `-`. ## Wait synchronously or poll Add `"sync": true` for short, interactive tasks. StefanBrain holds the connection for up to about 90 seconds, counted from when it receives the request, and answers as soon as the run ends. If the run takes longer, the request returns the ordinary `202` response and continues in the background. Set your HTTP client's timeout to 120 seconds so it receives that response. Poll the returned status URL every 3–10 seconds until the run is terminal. Polling does not count against rate limits. Use synchronous mode for short interactive requests. Use polling for long work, batch pipelines, or environments where holding a connection is fragile. ## Follow progress events The events endpoint returns turn phases, tool activity, and text sections. Page through events with the cursor returned as `next_after`: ```http theme={"system"} GET /api/developers/v1/runs/{run_id}/events?after={last_event_id} ``` Each page returns up to 200 events. Pass `next_after` as `after`. When a page is empty, `next_after` is `null`; keep your previous cursor. `is_done` is `true` on the page that contains the terminal event. The same endpoint supports server-sent events. Send this header: ```http theme={"system"} Accept: text/event-stream ``` Each SSE `id` is an event identifier. Heartbeats arrive approximately every 15 seconds, and the stream closes after the terminal event. Reconnect with `Last-Event-ID` or `?after=` to resume. Cursor polling and SSE contain the same event data; choose one transport for each client. ## Attach files Send `multipart/form-data` when StefanBrain must read files with the message. * Put the JSON request body in a `payload` field. * Add every upload as a `files` field. * A run accepts up to 10 files. * Each file can be up to 100 MB. * Supported inputs include images, PDFs, common Office documents, spreadsheets, and text files. * Audio and video files are rejected. Host the media and put its public URL in `message` instead. ```bash theme={"system"} 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` parts for run attachments. Internal attachment identifiers and referenced document identifiers are not part of this public run request. ## Request structured output Add `output_config.format` to constrain the final answer to a JSON schema. The raw text remains in `output`; parsed JSON returns in `structured_output`. Structured output uses strict mode: 1. The root schema must use `"type": "object"`. 2. Every object must set `"additionalProperties": false`. 3. Every object must list all property names in `required`. To make a value optional, include `null` in its type. ```json theme={"system"} { "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 } } } } ``` An invalid strict-mode schema returns `400 invalid_output_config` and identifies the failing path. `structured_output` is `null` when the run is unfinished or its final text is not valid JSON, including refusals and hard failures. ## Model behavior There is no model selector. If a request includes a `model` field, StefanBrain ignores it. Agent Runs execute the model that currently operates the StefanBrain product. See the generated REST reference for exact request and response schemas. See [Errors and retries](/developers/errors-and-retries) before adding retry logic. # Attachments Source: https://docs.stefanbrain.com/developers/attachments Upload files to an Agent Run with multipart form data. Use `multipart/form-data` on `POST /api/developers/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. Each file can be up to 100 MB. * Supported uploads include images, PDFs, common Office documents, spreadsheets, and text files. * Audio and video files are rejected. Host the media and put its public URL in the run's `message` instead. ```bash theme={"system"} 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. ## Handle upload errors * `400 too_many_attachments`: send 10 files or fewer. * `400 unsupported_attachment`: the file type is not supported, or the file is audio or video. * `400 invalid_multipart` or `invalid_json`: rebuild the multipart body with a valid JSON `payload` field. * `413 attachment_too_large`: a file is over 100 MB, or the whole request is too large. # Authentication Source: https://docs.stefanbrain.com/developers/authentication Create StefanBrain API keys, authenticate requests, and restrict keys with scopes and budgets. Create API keys in **Settings → Developers**. You must accept the current Developer API terms each time you create a key. StefanBrain shows the complete secret once, at creation. ## Authenticate a request Send the key as a bearer token: ```http theme={"system"} Authorization: Bearer stefan_sk_your_key_here ``` You can instead use the API key header: ```http theme={"system"} x-api-key: stefan_sk_your_key_here ``` An API key has the access of the account that owns it. A missing, invalid, revoked, or unauthorized key returns `401 Unauthorized`. A `stefan_oat_...` OAuth token from a connected assistant also works on every REST route under `/api/developers/v1`. OAuth tokens have no scopes and no per-key budget. On a standard member account, their usage bills the plan's monthly usage pool. See [Pricing](/developers/pricing). Keep API keys server-side. Do not expose them in browser payloads, logs, prompts, or committed configuration files. ## Restrict a key with scopes When creating a key through `POST /api/developers/keys`, use `scopes` to limit the surfaces that key can access. This endpoint needs a signed-in StefanBrain browser session; an API key cannot call it. The body takes `name`, `developerTermsAccepted: true`, `developerTermsVersion`, `scopes`, and `monthly_budget_cents`. `developerTermsVersion` must match the current terms version; otherwise the endpoint returns `400` with `requiredDeveloperTermsVersion`. The Settings form does not yet expose scopes or budgets. Supported scope values are: * `runs` * `tools` * `jobs` * `mcp` A scoped key receives `403 api_key_scope_forbidden` on other surfaces. Unknown scope values return `400`; StefanBrain does not ignore them. Omit `scopes`, or send an empty array, to create a full-access key. Keys created before scopes were introduced have full access. The job-polling endpoints accept a key with either the `tools` or `jobs` scope. A tools-scoped key can poll any job in the account's chats. The [feedback](/developers/feedback) endpoints accept a key with any scope. ## Set a per-key budget `monthly_budget_cents` caps one key's wallet-billed token spend, at the published rates, for one billing cycle. Image generation, search, and other per-use charges do not count toward it; the account wallet still limits them. The check runs before each request, so the request that crosses the limit can finish over it. At the limit, requests return `429 api_key_budget_exhausted` with `key_budget.budget_cents`, `key_budget.billed_cents`, and `key_budget.resets_at`. Omit `monthly_budget_cents` for no per-key limit. The account's API wallet still limits total REST spend. Scopes and budgets are useful for keys issued to a service, integration, or team member. For example, a CI key can use the `tools` scope with a \$10 monthly limit. See [Errors and retries](/developers/errors-and-retries) for the complete error envelope and retry guidance. ## Replace an API key or disable a compromised key Replace a key when its owner changes, an integration needs a separate credential, or the secret may have been exposed. If a key was exposed, revoke it immediately. For a planned rotation, test the replacement before revoking the old key. ### Create the replacement 1. Open [Settings → Developers](https://stefanbrain.com/settings/developers). 2. Create a key with a name that identifies the integration. 3. Accept the current Developer API terms. 4. Store the secret in your application's server-side secret manager. The full secret is shown only once. If the old key had scopes or a budget, create the replacement through `POST /api/developers/keys` with the same values; the Settings form does not set them. Never put the key in browser code, a screenshot, a support email, or a committed file. ### Update and test your integration Update the integration's stored credential and reload or redeploy it as needed. Check every environment that used the old key. Test authentication with a read-only request: ```bash theme={"system"} curl --fail-with-body https://stefanbrain.com/api/developers/v1/tools \ -H "Authorization: Bearer $STEFANBRAIN_API_KEY" ``` This check requires a key that can access tools. A successful response proves access to the catalog, not that every workflow works. ### Disable the old key Return to Developer settings, identify the old key by its name and displayed prefix, and use its **Delete** control. Requests that still use the old key will fail. Check scheduled integrations as well as the application you just tested. For a `401` response, check that the integration actually loaded the replacement secret. For scope or budget errors, see [Errors and retries](/developers/errors-and-retries). If you need support, send the key's name and the error code to [support@stefanbrain.com](mailto:support@stefanbrain.com)—never the secret. # CLI Source: https://docs.stefanbrain.com/developers/cli Install the StefanBrain CLI and script asks, tools, jobs, files, and agent runs with stable JSON output and exit codes. `sb` is the official StefanBrain command line. It wraps the TypeScript SDK and is designed for humans or coding agents running work from a shell. It requires Node.js 20.9 or later. ## Install and sign in ```bash theme={"system"} npm install -g @stefanbrain/cli sb login sb whoami ``` `sb login` signs you in through your browser with OAuth and PKCE. It receives the result on the loopback address `127.0.0.1`, so the browser must run on the same machine. The stored credential uses the member's plan usage and refreshes itself. The CLI stores the login in `$XDG_CONFIG_HOME/stefanbrain/credentials.json`, or in `~/.config/stefanbrain/credentials.json` when `XDG_CONFIG_HOME` is unset. The path is the same on every operating system, and the file has mode `0600`. `sb logout` revokes the stored login and removes it from the file. For containers, continuous integration, SSH servers, or another headless environment, set an API key instead: ```bash theme={"system"} export STEFANBRAIN_API_KEY="stefan_sk_your_key_here" sb whoami ``` The environment key takes precedence over a stored login and uses the prepaid API wallet. `sb whoami` prints the active credential, the account behind it, and why that credential won. The CLI has no credential flag because command-line arguments can be retained in shell history and process listings. ## Run common workflows ```bash theme={"system"} # One complete StefanBrain turn sb ask "Give me five Meta ad hooks for a sleep supplement." # Discover and invoke one live tool sb tools list sb tools describe web_search sb tools invoke web_search --input search.json # Submit, wait for, and download an asynchronous result sb jobs submit static_ad --input payload.json sb jobs wait static_ad --chat chat_your_chat sb jobs artifacts static_ad \ --chat chat_your_chat \ --out ./creatives # Upload a file and use its returned identifier sb files upload ./brief.pdf sb ask "Summarize this brief into three angles." \ --file-id upload_your_file # Stream an Agent Run sb runs start "Research this category." --follow # Tell StefanBrain where the API got in your way, then read the reply sb feedback send "POST /v1/runs returned 500 when output_config had a json_schema" \ --category bug sb feedback get ``` `sb jobs submit static_ad` runs the `create_images` tool. Put its arguments in `payload.json`: ```json theme={"system"} { "request": { "kind": "ad", "brief": "Bold before/after static ad for a sleep supplement", "outputs": { "mode": "count", "count": 1 } } } ``` To edit an image, run `sb tools invoke edit_image --input edit.json`. Job identifiers from submit tools are UUIDs. The acceptance on `stdout` carries the `job_id` and `chat`, and `stderr` prints the exact `sb jobs wait` command to run next. JSON payloads come from `--input file.json` or `--input -` for standard input. The CLI does not accept an inline JSON payload in command-line arguments. ## Command reference ```text theme={"system"} sb login sb logout sb whoami sb version sb init --project proj_... [--session