For agents
Service-account API keys, the MCP endpoint and its tools, idempotent retries, and the contract-version promise — how a program calls this product, not a person.
Everything above this page describes a report a person opens in a browser. This page describes the same product called by something else — your own script, agent, or scheduled job — issued its own credential, scoped to only what it needs, and able to retry without paying twice.
Service-account API keys
A key authorizes a program without handing it a human's login. Issue one from the account's API keys page (or POST /api/account/api-keys while signed in with a real session — a key cannot create or revoke its own siblings). It is shown once, as bsk_live_<random>; only its SHA-256 hash is ever stored, and every later view shows just the last four characters.
GET /api/account/api-keys— every key on file, maskedPOST /api/account/api-keys— { name, scopes: ["run_pulls", "read_reports"] } -> { key, api_key }DELETE /api/account/api-keys?id=...— revoke one key, permanently
| Scope | Grants |
|---|---|
run_pulls | May quote and start a comprehensive pull and spend the account's credits, within the alpha limits. |
read_reports | May poll a pull's status and read a finished report or brand kit. Cannot start one. |
Issue narrow keys
A key that could do both is barely better than the session token it replaces. Issue narrow keys — one for the job that starts pulls, a different one for the job that only reads them — and revoke either without touching the other.
Retries that do not charge twice
POST /api/comprehensive (and the MCP run_brand_pull tool, which calls the identical handler) accepts an optional idempotency_key, unique per account. A retry sent with the same key — a flaky network, a queue redelivery, an agent that resends on timeout — returns the ORIGINAL pull: the same pull_id, HTTP 200, and never a second reservation or a second charge. The guarantee is enforced at the database, not only in application code: a partial unique index on the pull row is what makes a genuine race between two concurrent identical requests collapse to one pull rather than two.
The MCP endpoint
POST /api/mcp speaks Streamable HTTP MCP in its simplest legal shape: one JSON-RPC 2.0 object in, one out, no session id, no SSE stream to keep open. Hand-rolled, not the SDK — there are exactly three methods to implement.
| Field | Value |
|---|---|
| Endpoint | POST /api/mcp |
| Auth | Authorization: Bearer — a service-account key (bsk_live_..., described above) or the token an assistant receives when it connects through OAuth (Claude or ChatGPT; setup on /mcp), on every call including initialize. An OAuth assistant is read-only. |
| Methods | initialize, tools/list, tools/call |
Tools
| Tool | Scope | Does |
|---|---|---|
list_brands | read_reports | Every saved brand with its latest free read, its weekly checks and schedules, its page-list read and its latest report ID. |
get_brand_basics | read_reports | One saved free read in full: the graded palette with its evidence, typography, logos, icons, homepage images, social profiles, links and the methods that made the read. |
get_brand_changes | read_reports | What changed between a saved brand's free read and the read before it. |
list_brand_changes | read_reports | What changed across the account's saved brands in the last days. |
get_site_pages | read_reports | The pages a saved brand lists in its own sitemaps, and what changed since the previous read. |
get_social_changes | read_reports | What changed between a saved brand's two latest social snapshots. |
get_search_changes | read_reports | What changed between a saved brand's two latest search snapshots, and whether newer Google data exists. |
get_brand_monitoring | read_reports | Everything saved about one brand as one Markdown document. |
list_brand_reports | read_reports | The account's saved full reports. |
get_brand_report_overview | read_reports | A saved report's overview. |
get_brand_report_section | read_reports | A bounded page of saved evidence in one area of a report. |
get_brand_media | read_reports | Preserved image and video references from a report, with their provenance. |
open_brand_report | read_reports | Opens a saved report as an interactive app in the chat. Never starts collection. |
get_brand_report_view_chunk | read_reports | One display chunk of a saved report, used by the interactive app. |
quote_brand_pull | run_pulls | A zero-spend preview of a pull: its scope, plan hash and maximum provider cost. No collection and no charge. |
run_brand_pull(url, pull_plan, seo_market, plan_bindings, plan_hash, approved_max_cost_usd, tier?, idempotency_key?) | run_pulls | Validates the same reviewed plan as the website, reserves the durable queue job and returns its pull_id. Missing or stale approval is refused before collection. |
get_pull_status(pull_id) | read_reports | Running, passed, partial, or failed, with per-leg progress. |
get_brand_report(pull_id) | read_reports | The finished capture_result, stamped with contract_version. |
get_brand_kit(pull_id) | read_reports | A download URL for the brand kit .zip. An OAuth connection receives a report link instead. |
First request a zero-spend quote at POST /api/query-manifest using pull_plan, seo_market, plan_bindings and tier. Review the returned scope, hash and maximum provider cost before supplying that exact approval envelope to run_brand_pull. The server independently enforces the operator-approved user, plan, cap and one-run approval ID; a quote does not enable spending. Work runs in the shared durable queue, never inline. Scope refusals name the missing scope. Customer credits remain separate from provider dollars, and unsupported or parked selections cannot run.
The versioning promise
Every report response — the status poll once a pull has evidence, and the MCP get_brand_report tool's own payload — carries contract_version: "brand-studio-report/v1". That is a promise about the OUTER envelope a third party may build against, not a restatement of the dozen internal schema_version fields already carried inside capture_result and its collectors (those describe internal shape and are unchanged by this promise). A breaking change to what a report response carries ships as v2, with a deprecation window, rather than as a silent edit under a consumer already depending on v1.
The full text of this page is also served at /llms.txt, in the shape a model reading it directly expects.