# BRIEFBUSTER

> BRIEFBUSTER is an AI-assisted tool for busting open creative briefs: submit a brief, get back a hand of "cards" — real campaign examples, creative moves, cognitive biases, trends and more — then pin keepers to a session's canvas, generate "what if" provocations, fuse ingredients into campaign ideas, and arrange everything into a shareable board. Agents get the core creative tools through an MCP server and broader functionality through a REST API, both authenticated with the same per-user API key.

Key facts for agents:

- Base URL: https://briefbuster.co — REST under `/api/*`, MCP server at `/mcp`.
- Auth: API key, created on the Account page (https://briefbuster.co/account, sign-in required — a human
  signs in with an emailed code or with Google; /api/public/signup reports the current signup posture). Send as `x-api-key: <KEY>` or `Authorization: Bearer <KEY>`. A key acts AS ITS OWNER, in the workspace it was created in — sessions and pins you create appear in that user's web UI.
- Every card carries a `gist` — a one-sentence summary; public detail lives in the card's `data`; factory example/move transcripts, scenes and private craft details stay server-side. Brand voice-of-customer verbatims are public card detail within the workspace's access scope.
- Workspaces can add SIGNATURE PACKS — their own cards (type `custom`, `data.description` markdown + `data.link_url`), private to the workspace. Pack slugs are extra `mix` keys in retrieve calls; `list_sessions` (MCP) and `GET /api/workspace/decks` return them (route paths keep the historical `decks` wire name). Signature cards ride retrieval only — search covers the factory library.
- Input that can't be used answers HTTP 422 with `"buster": true` in the body — most often a brief rejected as unusable; reword and retry.
- Rate limits answer 429 with a `Retry-After` header. Budgets are per USER (your keys share one, they don't multiply it) over three windows — minute, hour and day; the day ceiling applies to the routes that cost us money (LLM calls, ideation, uploads, canvas renders) and to library reads (search_cards / get_card, i.e. GET /api/search, /api/card/:id: 30/min, 300/hr, 1,000/day). Cards themselves are metered as well: every call that deals library cards (retrieve, search, card by id, a session bust, explore_wider) counts the cards returned against a per-user budget — 2,000/day and 10,000/month on a trial, 4,000/day and 30,000/month on a paid workspace — and answers 429 with scope `cards:day` / `cards:month` when it's spent. A working session never gets near it (a heavy day is under 1,000 cards). explore_wider takes at most 10 chapters per exploration; after that, bust a fresh angle. A workspace whose subscription lapsed answers 402 on writes AND on library browsing (search_cards / search_trends / get_card, i.e. GET /api/search, /api/trends, /api/card/:id, /api/packs/factory/:type/cards) — those need an active workspace. Reads of your OWN work keep working (sessions, canvases, attachments, signature packs, brands — list_brands stays readable). Permitted cleanup, including REST DELETE and MCP delete_session, also remains available; creator/admin authorization still applies.

## MCP server

Streamable HTTP, stateless (POST returns JSON; no session id). Connect from Claude Code:

    claude mcp add --transport http briefbuster \
      https://briefbuster.co/mcp \
      --header "Authorization: Bearer YOUR_API_KEY"

Or in a JSON MCP client config: `{"type": "http", "url": "https://briefbuster.co/mcp", "headers": {"Authorization": "Bearer YOUR_API_KEY"}}`.

Tools:

- retrieve_cards(brief, mix?, workspace_brand_id?) — brief → one interleaved hand of cards across card types; the main inspiration tool. `mix` = cards per key, e.g. {"move": 8, "example": 20, "our-research": 5}; keys are the factory types plus the workspace's signature-pack slugs; 0 excludes one. `question` (session-only) and `custom` (use the pack's slug) are rejected, not ignored. Omit `mix` and you get the workspace's own blend, scaled to fill a whole hand; a `mix` you pass is used verbatim, not rescaled. workspace_brand_id: with an ACTIVE Brand Intelligence pack, that brand's harvested feeds (competitor angles `comp`, category conventions `norm`, customer voices `voc`) join the deal ON TOP of the factory blend, pooled under three extra mix keys — `brand` / `competition` / `voices` — and the hand grows to 60; keepers join the brand dial even without an active intelligence pack.
- search_cards(query, type?, medium?, source?, move_kind?) — semantic search with filters (factory library; signature packs surface via retrieve_cards). Requires an active workspace (library browsing, not "your own work").
- search_trends(query?, limit?) — query the live TRENDSHROOMS feed of freshly-harvested cultural phenomena (spiky, ephemeral trends, distinct from the timeless library). With query → semantic search; without → the most recently spotted. Only trends from the last ~8 weeks surface. Each returns what/provocation/evidence (dated links)/spotted. Requires an active workspace (the live feed is not among the reads a lapsed workspace keeps).
- get_card(id) — one card as the app shows it: gist, insight, brief, idea, the working note, media, and its type's detail rows in `data`. The private factory craft layer (transcripts, scenes and mechanism) stays server-side — it feeds what_if / fuse / add_hex directly.
- what_if(brief, card_id, session_id?, deliverable?, workspace_brand_id?) — apply a card's move to a brief → a concrete provocation. deliverable: a free-text phrase ("a radio spot") that forces the provocation to BE that artefact, not a territory around it; omit for unconstrained. workspace_brand_id: a brand whose creative DNA (hard voice rules, traits, exemplars, lexicon) seasons the output; defaults to the session's bound brand when session_id is given. Returns {what_if, brand} — brand = the name whose DNA applied, or null.
- fuse(ingredients, brief? | session_id?, deliverable?, workspace_brand_id?) — collide up to 12 ingredients (cards and/or free-text, optionally weighted) into ONE campaign idea {name, desc, brand}. deliverable + workspace_brand_id: same semantics as what_if.
- list_brands() — the workspace's brands (free objects, docs/BRANDS.md): id, name, profile (markets/vertical/website/competitors as [{name, note?}]), creative DNA (voice: constraints/traits/exemplars/lexicon/truth; creative memory: platform = the enduring campaign idea, avoid = burned ground never proposed again, works = proven learnings used as a springboard), and `pack` — the Brand Intelligence state (null = none; `market`/`activated_at` present = the brand's harvested feeds join retrieval wherever the brand is active). Brand ids feed create_session/update_session/what_if/fuse/retrieve_cards.
- list_sessions(limit?, offset?) — the workspace this key acts in + its signature packs (slug/name/description — the slugs are valid retrieve_cards mix keys) + a page of readable sessions there (limit default/max 100, offset default 0; returns total/limit/offset alongside sessions; own + teammate-shared; `mine: 0` = a teammate's, co-editable when workspace-shared).
- create_session(brief, workspace_brand_id?) — start a named session; returns the first hand of cards + its `root_id`. workspace_brand_id binds a brand — its DNA then seasons every what_if/fuse in the session.
- get_session(id) — brief, search history, pinned cards, attachments; `can_edit` says whether content writes (pins, what-ifs, brief, searches) will stick, `can_write` whether you own it (settings: title, sharing, delete).
- update_session(session_id, title?, brief?, visibility?, deliverable?, workspace_brand_id?) — edit the session. brief + deliverable + workspace_brand_id are content (any co-editor); title + visibility are creator-only. deliverable null/"" rotates vessels per idea; workspace_brand_id null unbinds the brand.
- delete_session(session_id) — permanently delete a session you own (or workspace owner/admin). Cleans up pins, searches, attachments, and the live BoardRoom.
- add_brief(session_id, brief) — run another search in a session → a fresh hand of cards.
- explore_wider(session_id, root_id?) — LLM reframes the search along a lateral angle and retrieves fresh cards excluding everything already seen. Repeatable, up to 10 chapters per exploration (then it errors in Buster's voice — add_brief a fresh angle).
- read_attachment(attachment_id, offset?) — extracted text of an attached document, paged.
- share_session(session_id, action? | visibility?, share_attachments?) — public canvas link (share/rotate/unshare) and workspace sharing (private/workspace).
- remix_session(session_id) — copy a readable session into a new one the key's owner controls.
- pin_card(session_id, card_id, what_if?, brief?) / unpin_card(session_id, card_id) — pin a card to the session's canvas (auto-placed) / remove it.
- add_note(session_id, text, color?, x?, y?, w?, h?) — pin a post-it note. Omitted x/y deals it into the next open grid slot. color is one of the marker names: ink, yellow, red, orange, green, teal, blue, purple, pink, brown (shared/palette.json).
- add_text(session_id, text, color?, x?, y?) — a headline-sized marker scribble (≤500 chars — a word or two, not a note's paragraph).
- add_sticker(session_id, shape, color?, x?, y?, s?, r?) — drop a sticker. shape is one of STICKER_SHAPES (shared/stickers.ts): airplane, angry-albatross, angry-beaver, angry-spider, arrow, arrow-2, blot, cat, cat-2, confusion, crap-cloud, cupcake, eyeglasses, fear, heart-arrow, key, middle-finger, mouth, music, oval, oval-2, poop, question-bubble, star, sticker, target, toilet-paper — an unrecognized shape is rejected, not dropped silently. s scales 0.2–4, r rotates −180 to 180 degrees.
- add_hex(session_id, links, idea?, deliverable?, x?, y?) — drop a loaded idea machine: a hex wired to up to 6 elements already on the board (pinned cards, notes, scribbles, images, sketches, links, doc chips — never stickers or other hexes), holding one campaign idea ideated from their content + the session's brief exactly as a hand-made hex would. Pass idea {name, desc} (e.g. a fuse result) to seat it instead of ideating; empty links = a brief-only hex; omit x/y and it lands beside its ingredients. The user can reroll, rewire, cut and lock it like any hex — and an agent can rewrite or lock it via edit_element (idea/locked). Returns the new id, position and idea.
- place_elements(session_id, moves) — batch-arrange up to 50 elements in one call; moves = [{id, x?, y?, s?, r?, w?, h?}]. Works on any node, including pinned cards and the brief (transform fields only for those two and existing sketches — w/h are measured, not settable). Returns each element's final transform.
- edit_element(session_id, id, text?, color?, shape?, alt?, idea?, locked?) — edit an existing element's content; which fields apply depends on its type (note: text/color, sticker: shape/color, image: alt, hex: idea {name, desc} + locked — true freezes the idea against rerolls; the padlock in the hex chrome stays the user's override). Fields that don't apply to the element's type are rejected, not ignored.
- remove_elements(session_id, ids) — delete up to 50 elements. Refuses pinned cards (use unpin_card, which also cleans up the pin) and the brief.
- get_canvas(session_id) — see the canvas as structured data: every element (brief, cards, hexes + ideas + connections, notes, scribbles, stickers, images, sketches, links, doc chips) with content, position and size, plus spatial hints (clusters, overlaps — element pairs likely stacked on each other, judged over estimated sizes; stickers exempt — near_brief, bounds). Sizes are estimates where no client has measured, so the hints flag candidates, they don't certify a clean board — render the canvas to check the look. Reads the live board when one's open (not the cached snapshot), so writes — yours or anyone's — are always current. Returns `live` alongside `version`. The primary way to plan moves.
- render_canvas(session_id, w?, h?) — a faithful PNG snapshot of the whiteboard as a human sees it, returned as an MCP image block + metadata text block. On a live board, flushes pending writes to storage first, so the render is never stale mid-edit. Cached until the session changes; a cold render takes a few seconds. Default 1024×640, clamped 320–2000.

## REST API

JSON over HTTPS. REST is a **superset** of the MCP toolset: everything MCP can do has a REST twin, plus signature-pack admin, attachment upload, link extract, the browser-session-only appearance preference (PATCH /api/account/appearance with {playfulCardAngles:boolean}; public shares/renders use the session creator's value), house style (PATCH /api/workspace/style) / card-mix (PATCH /api/workspace/mix) writes, anatomy recompute (GET|PATCH /api/sessions/:id/facets, POST /api/sessions/:id/facets/recompute), and the whole-blob canvas layout PATCH /api/sessions/:id/canvas (live boards refuse layout PATCH with 409 {live: true} — the realtime doc owns them; use the /elements routes instead). Element-level canvas writes are NOT REST-exclusive: MCP's add_note/add_text/add_sticker/add_hex/place_elements/edit_element/remove_elements share one write path with REST's POST/DELETE/GET /api/sessions/:id/elements (add_hex's REST twin is the two-step: POST /api/sessions/:id/ideate for the idea, then a type:"hex" element create seating it with links). Either way, a write on a board someone has open lands live on every connected client, shown as a 🤖 chip in the who's-here bar (never as the key owner's own presence); on a board nobody has open it applies straight to the stored canvas. These writes are gated like any other write — a lapsed workspace's key can't use them.

Cards:

- POST /api/retrieve {brief, mix?, workspace_brand_id?} — brief → a hand of cards. mix keys = factory types + your signature-pack slugs; 0 excludes one (`question` and `custom` are rejected — the former is session-only, the latter isn't a key). Precedence: house defaults < the workspace's saved mix < the mix you pass. The first two are scaled to fill a whole hand; the mix you pass is used verbatim. workspace_brand_id: with an active Brand Intelligence pack the brand's harvested feeds join the deal on top of the factory blend (extra mix keys `brand`/`competition`/`voices`, hand grows to 60); keepers join the brand dial even without an active intelligence pack.
- GET /api/search?q=&type=&medium=&source=&move_kind= — semantic search (factory library). Active workspace required (library browsing 402s when lapsed).
- GET /api/trends?q=&limit=&offset= — the live TRENDSHROOMS feed (last ~8 weeks; active workspace required). q → semantic search; omit q → recent, newest first, paginated via offset. Returns {mode, trends}.
- GET /api/card/:id — one card as the app shows it (see get_card above). Active workspace required.
- POST /api/apply {card_id, brief?, session_id?, collide?, deliverable?, exclude_deliverable?} — what-if generation (`collide` folds in a random wildcard card for a fresh angle). `deliverable` = a free-text phrase forcing the provocation to BE that artefact (defaults to the session's pinned vessel, or a rotated one when unpinned); `exclude_deliverable` won't repeat a given vessel. Returns {what_if, deliverable, brand} — the vessel used + the brand whose DNA seasoned it (the session's bound brand; null when none).
- POST /api/cards/:id/report {reason?, session_id?} — flag a broken factory card for admin triage. One live report per user per card; a re-report rewrites the reason.

Sessions:

- POST /api/sessions {brief, attachment_ids?, deliverable?, workspace_brand_id?} — create (auto-named); returns the first hand of cards + rootId. deliverable pins "what we're making" from the start (free text, ≤120 chars); omit to rotate a vessel per idea. workspace_brand_id binds a brand (GET /api/workspace/brands for ids) — its creative DNA then seasons every what-if/hex in the session.
- GET /api/sessions?limit=100&offset=0 — paginated readable sessions (own + workspace-shared), newest first. Returns {sessions, total, limit, offset}; limit 1–100.
- GET /api/sessions/:id — brief + queries + pinned + attachments (+ can_edit / can_write).
- POST /api/sessions/:id/query {brief, mix?} — new search → a fresh hand of cards. `mix` rebalances THIS search only (same keys as /api/retrieve, plus `question` = how many Wonder Weeds ride beside the hand; 0 excludes); omit it and the workspace's saved mix applies. The resolved mix is stored on the chapter, so re-opening the session shows the cards it dealt.
- POST /api/sessions/:id/explore-wider {root_id?, mix?} — lateral reframe → fresh chapter. Without `mix` the chapter inherits its root's blend.
- PATCH /api/sessions/:id {title?, brief?, visibility?, deliverable?, workspace_brand_id?} — visibility: private | workspace. deliverable = what the session is making (a free-text phrase; null or "" rotates a different vessel per idea). brief + deliverable + workspace_brand_id (bind a brand; null unbinds) are content writes (any member on a shared session); title/visibility are creator-only. Workspace-shared sessions are co-editable: content routes (pins, query, explore-wider, ideate, brief, facets) accept any member; settings routes stay creator-only.
- DELETE /api/sessions/:id
- POST /api/sessions/:id/pins {card_id, what_if?, brief?, deliverable?, at?} / DELETE /api/sessions/:id/pins/:cardId — pin / unpin a card on the canvas (`deliverable` = the vessel a stored what-if was made for; `at` = {x, y} canvas coordinates, omit and the card is auto-placed in the next free slot below the brief).
- PATCH /api/sessions/:id/canvas {canvas} — replace the whole saved canvas layout blob. Answers 409 {live: true} once a board has gone live (someone has it open) — the realtime doc owns the layout then; use the /elements routes instead.
- POST /api/sessions/:id/share {action: share|rotate|unshare, share_attachments?} — public canvas link at /c/<uuid>.
- POST /api/sessions/:id/remix — copy into a session you own.
- POST /api/sessions/:id/ideate {inputs: [{kind, text, weight}], deliverable?, exclude_deliverable?} — fuse brief + inputs into one campaign idea. `deliverable` / `exclude_deliverable` shape + rotate the vessel exactly as /api/apply; returns {name, desc, deliverable, brand} — the vessel used + the brand whose DNA seasoned it (null when the session has none).
- POST /api/sessions/:id/elements {elements: [...patches]} — create or update canvas elements: notes, marker text, stickers, images, links, hexes (a type:"hex" create may carry idea {name, desc}, links — up to 6 on-board element ids — and deliverable), or move/resize/rotate anything already on the board (pinned cards and the brief included, transform fields only for those two and existing sketches). Up to 50 patches per call; omit a new element's x/y and it's dealt into an open spot. Lands live for anyone with the board open; otherwise applied straight to the saved canvas. Returns each element's final position.
- DELETE /api/sessions/:id/elements {ids: [...]} — remove canvas elements, up to 50 per call. Refuses pinned cards (unpin via /api/sessions/:id/pins/:cardId) and the brief.
- GET /api/sessions/:id/elements — the canvas as structured data, same shape as MCP's get_canvas. Reads the live board when it's open.
- Brands (docs/BRANDS.md — free workspace objects). Generation reads the STRUCTURED brand: profile facts (positioning, competitors, vertical/markets) + creative DNA (hard voice rules, traits with contrast, exemplar copy, lexicon, brand truth). Raw uploaded documents are stored as sources and never injected.
  - profile.vertical is a slug from the fixed industry vocabulary (unknown values are dropped): cosmetics-personal-care, baby-kids, fmcg-food, fmcg-beverage, alcohol, auto, fintech-banking, insurance, telecom, tech-electronics, app-software, gaming, gambling-betting, toys-play, fashion-apparel, luxury, retail-ecommerce, travel-tourism, hospitality-food-service, health-pharma, fitness-wellness, home-living, media-entertainment, sports, education, ngo-public-service, politics, b2b-services, energy-utilities, logistics, real-estate, pets, other.
  - GET /api/workspace/brands — list (members); POST — create {name, profile?, dna?} (owner/manager). profile.competitors = names or [{name, note?}] (note = your read of the rival); dna = {constraints, traits, exemplars, lexicon:{loved,banned}, truth, platform?, avoid[], works[]} — platform = the enduring campaign idea, avoid = burned ground (clichés + already-tried; never proposed), works = proven learnings (a springboard, not a ceiling). All of it seasons every what-if/hex/fuse on a bound session.
  - GET /api/workspace/brands/:id — one brand; PATCH {name?, profile?, dna?, sources?} (owner/manager; human-edited DNA keys are soft-locked against auto-distillation; create/PATCH reject oversized profile/DNA/source lists with 400 before any mutation); DELETE (owner/manager; bound sessions fall back to no brand).
  - POST /api/workspace/brands/:id/docs?filename=… — upload a brand document (raw body; pdf/word/powerpoint/excel/rtf/epub/md/txt ≤30MB). Parsed to text server-side, original retained; stored as a source (with `key` + `size`). Returns the updated brand.
  - GET /api/workspace/brands/:id/docs/:docId — download a doc's retained original (docId = last segment of the source's `key`; members).
  - POST /api/workspace/brands/:id/distill — propose a FULL creative-DNA rewrite from everything on the brand (uploaded docs + pastes + profile). With no documents yet and a website on the profile, the route first reads the brand's own site (homepage + up to 4 story pages) and saves the pages as `site` sources; then `{dna, brand?, read_website}` — the proposal only, persist by PATCHing the brand; 422 when there's nothing to work from (owner/manager).
  - POST /api/workspace/brands/:id/memory {kind: "avoid"|"works", name?, desc?, text?, source?: {session_id, element_id}} — the brand remembers something from the work: appends ONE line ("name" — first sentence, ≤400 chars) to burned ground or what-works; deduped; 30 lines per list (400 with buster copy when full). Managers, or anyone who can edit the `source` session when it is bound to this brand. Returns {brand, line, duplicate?}.
  - POST /api/workspace/brands/:id/keep {name?, desc, source?} — keep an idea for the brand: saved as a card in the brand's keepers pack (created on first keep; never on the signature shelf or in the generic mix), embedded at once, dealt on the `brand` dial whenever the brand is bound — pack or no pack. Returns {card_id, deck}. Full title/text/formatting are stored for browsing/search/export; retrieval uses a bounded excerpt. With source.session_id + source.element_id, saves update that element even after renaming; different elements never merge by title. Without both IDs each save creates a new card. Legacy cards remain separate. Same access rule as memory.
  - GET /api/workspace/brands/:id/sessions?limit=30&offset=0 — paginated sessions bound to this brand that you can see (own + workspace-shared): {sessions: [{id, title, updated_at, visibility, creator, mine, pins}], total, limit, offset}; limit 1–100.
  - GET /api/workspace/brands/:id/keepers?q=&sort=newest|oldest&limit=24&offset=0 — search titles/full text by literal substring; limit 1–100. Returns {deck: {id, slug, name} | null, cards: [{id, gist, description, link_url, created_at}], total, limit, offset}. Session links are relative; paginate for all results (no total-card cap); DELETE /api/workspace/brands/:id/keepers/:cardId prunes one (owner/manager).
  - POST /api/workspace/brands/:id/enrich — fill EMPTY profile fields from a live brand search (website/vertical/positioning/competitors with a one-line note each; markets only when the brand name itself carries a scope like "Nike Germany"), plus dna.platform when the brand has a famous line and the slot is empty and untouched. Returns {brand, found}; found=false = unchanged (owner/manager).
  - Brand Intelligence: every brand carries a `pack` field (null = off; {market, activated_at, competitors} = on — its harvested feeds, competitor creative + category conventions + customer voices for that market, join retrieval wherever the brand is active). Self-serve, free in beta (€24/month per brand and market after beta, with your opt-in): POST /api/workspace/brands/:id/pack {market?} switches it on (owner/manager; market defaults to the profile's first; bounded by the workspace's atom allowance) — activation requests the first harvest and the activating user is emailed when cards land; DELETE the same path switches it off (everything gathered is kept). GET /api/workspace/brands/:id/pack-status → {state: off|harvesting|live, cards, kinds, last_run} — the harvest progress read. GET /api/workspace/brands/:id/pack-cards → {feeds: [{name, dial, cards}]} — browse everything gathered, grouped by feed.
- GET /api/sessions/:id/render?w=&h= — the canvas rendered to a PNG (image/png body), exactly as the web UI draws it. Defaults 1200×630; w 320–2000, h 200–2000. Cached until the session changes (ETag honoured); cold renders take a few seconds and are rate-limited at 10/min.

Signature packs (workspace-private cards; reads any member, writes owner/manager — route paths keep the historical `decks` wire name):

- GET /api/workspace/decks — packs (id, slug, name, description, color, cards) + default_k.
- POST /api/workspace/decks {name, description, color?, slug?} — create (slug defaults to slugified name; immutable after).
- PATCH /api/workspace/decks/:id {name?, description?, color?} / DELETE /api/workspace/decks/:id (deletes its cards everywhere).
- GET /api/workspace/decks/:id/cards — the pack's cards.
- POST /api/workspace/decks/:id/cards {gist, description?, link_url?} — add/update a card (gist is the only required field; same gist = update). Embedded on save; search visibility follows index propagation and may take a few minutes. The 2,000-card cap applies to new cards only; updates work at capacity.
- POST /api/workspace/decks/:id/cards/csv {csv} — bulk import; header row names columns (gist required), ≤500 rows per paste. Per-row errors report invalid/over-cap additions; existing cards can still be updated at capacity.
- DELETE /api/workspace/decks/:id/cards/:cardId
- PUT /api/workspace/decks/:id/cards/:cardId/image — raw-body image (png/jpg/webp/gif ≤10MB); DELETE removes it.

Attachments & canvas drops:

- POST /api/attachments?filename= — raw-body upload. Documents (pdf/docx/md, ≤30MB) stage for binding to a session; add &session_id= to upload a canvas image (png/jpg/webp/gif/svg, ≤10MB) bound to that session immediately. Image uploads get a vision-model description in `digest` (that's what canvas hexes hear).
- POST /api/sessions/:id/attachments {attachment_ids} — bind staged document uploads to an existing session (triggers an anatomy re-read).
- GET /api/attachments/:id/meta — extraction status / metadata poll for an upload.
- GET /api/attachments/:id — download; images serve inline and take ?w= (64–1600) for a resized webp.
- GET /api/attachments/:id/text?offset= — extracted text of a document, paged.
- DELETE /api/attachments/:id
- POST /api/links/extract {url} — read a URL for a canvas link node: {link: {url, title, summary, og_image}}. Pages are read by Firecrawl. A YOUTUBE url is watched instead — the model views the film and `summary` comes back as working notes plus timestamped beats, in the same shape. Videos that cannot be watched (private, deleted, or past the length cap) still return their title and thumbnail, with a `buster` line saying why. A URL that points at a file rather than a page (image, archive, font) returns 422 with buster: true — there is nothing to read; the link still works as a link.

Sharing note: a shared canvas is publicly readable as JSON at GET /api/public/canvas/:share (no auth — title, brief, canvas, pinned cards; document attachments only when the owner opted in). Images pinned to a shared canvas are always visible to link holders (GET /api/public/canvas-image/:share/:id); the share_attachments toggle gates documents only. A shared canvas also serves a public PNG render at GET /api/public/canvas-render/:share.png (same access as the share link; it doubles as the link's og:image).

## Docs

- [Developers page](https://briefbuster.co/developers): this reference, human-readable.
- [Model Context Protocol](https://modelcontextprotocol.io): the protocol the /mcp endpoint speaks.

## REST history, Buster and sketch contracts

These session routes accept the authenticated web session or the same API key as other REST routes. Keys use their owner's identity and current workspace membership. Reads require session access; Buster and sketch routes require content-edit access. Writes require an active workspace. There are no dedicated external MCP history/restore, Buster task/command/stop, or sketch tools. Brand management, memory, keepers, activation and attachment upload remain REST-only; `list_brands` and brand arguments are supported in MCP. Hex `edit_element` changes saved text; it does not reroll an idea.

- GET `/api/sessions/:id/versions` → `{versions: [{id, kind, created_at}]}` in chronological order. GET `/api/sessions/:id/versions/:vid` → `{version, canvas, brief, deliverable, pinned}`.
- GET `/api/sessions/:id/ideas?ref=<element-or-card-id>&kind=hex|what_if` → `{ideas}`. `ref` is required; kind is optional. Reads the newest 100 rows and deduplicates equal content, newest first.
- POST `/api/sessions/:id/restore` with `{version_id}` restores a checkpoint and returns `{ok:true}`. Creator-only; checkpoints current state first. No public-share access to these history routes.
- POST `/api/sessions/:id/sidekick` with `{text}` (up to 4,000 characters), or `{auto:true}`, starts a Buster task. Response is `text/event-stream`: JSON data frames begin with `{type:"task",id}` and continue with `tool`, `tool_done`, `say`, then `done` (status/reply/turns/cost_usd; status is done, wound_down, stopped or failed). Execution continues after disconnect. Add {background:true} for an immediate 202 {id,status}; GET /api/sessions/:id/sidekick/status?task=<id>&after=<sequence> returns private {task,events} for reconnecting (omit task for latest). Events contain sequence and event; task includes terminal reply/status. Auto completion may email the key owner; stopped runs do not send a completion email. A successful auto run with pinned work also marks that user's basic onboarding complete; failed, stopped, partial and empty runs do not. 402 can mean allowance exhausted, 403 workspace feature disabled, 409 this user already has a running task, 429 rate limit.
- GET `/api/sessions/:id/sidekick` → `{messages, allowance, mode, modes}`; transcript and mode are private to the calling user; messages contain role, at, kind (user/reply/work) and text. POST `.../sidekick/command` with `{command:"new"}` resets conversation, or `{command:"mode",value?}` reads/sets mode. DELETE `.../sidekick` also resets conversation. Neither resets task usage. Custom mode instructions have a 600-character maximum; oversized values return 400 without changing the saved mode. Custom conversation mode applies to replies and generated prose as well as ideation; a requested voice overrides the default dry tone and brevity, while truthfulness, privacy, safety, tool permissions and required formats still apply. Each task reads the current canvas before referring to existing elements. Small requests preserve unrelated content and layout; an explicit request to choose a favourite gets a choice. Full Autobust still develops and arranges distinct territories. POST `.../sidekick/stop` → `{ok,stopping}` requests the caller's running task stop between turns. POST `.../sidekick/quip` with `{trigger}` (pins, facets, idle or dry) returns `{quip}`; this is the web's ambient voice surface.
- POST `/api/sessions/:id/sketch` with `{idea, deliverable?, steer?, prev_prompt?, prev_att_id?, source_id?}` → `{attachment, headline, image_prompt, edited}`. `idea` is required (1,200-character excerpt); steer is capped at 300, previous prompt at 1,000. With steer and a readable same-session image, edits that image. Creates a generated attachment; placing the sketch node remains the web canvas's responsibility. 429 has `Retry-After`; 502 is generation failure. External MCP has no sketch generation or content-edit tool. Existing sketches are readable through get_canvas and movable through place_elements.

MCP retrieval and pinned summaries include public descriptions (up to 4,000 characters per detail field), comp/voc/norm detail and deck identity (slug/name/color/kind). get_card resolves owned and subscribed packs identically to REST; factory craft data stays server-side.

Structured canvas sketch contract: `get_canvas` and `GET /api/sessions/:id/elements` expose `kind: "sketch"`, `att_id`, `headline`, `subject`, `prompt`, `source_id`, optional `deliverable`, position, scale, size and rotation-aware occupied `bounds`. Width/height include scale; sketch height includes its caption. Geometry uses conservative sizing for unmeasured content and remains paired with a rendered visual check. Internal Buster tools `sketch_idea` (draw a source idea) and `arrange_territories` (deterministic grouped placement) are not part of the external MCP registry.

## Account and browsing helpers

- GET `/api/me` → `{user, org, appearance, apiKey, isAdmin}`; org includes entitlement and, when enabled, `sidekickTasks`. Authenticated browser session or API key; key requests expose a minimal user identity.
- POST `/api/me/onboarded` → `{onboardedAt}` (milliseconds), a one-way browser-only acknowledgement; repeats preserve the existing timestamp. API keys receive 403. This write requires an active workspace. Successful autobust completion can acknowledge onboarding server-side as described above.
- GET `/api/deliverables` → `{deliverables: string[]}`, the active Making labels in display order. Generation still accepts a free-text deliverable.
- GET `/api/packs/factory` → `{packs: [{type, label, description, color, cards}]}`. GET `/api/packs/factory/:type/cards` → `{cards}`, a shuffled sample of up to 12 cards; this sample requires entitlement and consumes library/card budgets. The metadata shelf remains readable when expired.
- GET `/api/decks/media/:cardId` serves an owned signature-card image. The share-gated public variant `/api/public/deck-media/:share/:cardId` requires the card to be pinned on that shared board.

Brand profiles also accept `personas: [{name, description}]` as reusable audience lenses. These are returned by brand reads and MCP `list_brands`; session Anatomy can copy a lens without changing the saved brand persona. Signature-pack, house-style and workspace-mix writes require owner/manager authority.

MCP transport: POST `/mcp` returns JSON-RPC, including batches; GET returns 405 (no SSE subscription). Notifications with no response return 202. Tool failures, including subscription_required and unusable briefs, normally use HTTP 200 with `result.isError: true` and text content. Authentication/rate failures use HTTP 401/429; malformed JSON uses 400. REST status codes such as 402/422 must not be assumed for MCP tool errors. Internal Buster uses SSE on the REST sidekick route; it is separate from MCP transport.

Canvas Hex weights are independent distance influence, not shares that must sum to 100. Each source uses round(100 × max(0.05, 1 − centre-distance / 700)); even one source weakens with distance. The ideate API preserves each supplied 0–100 weight without normalization. add_hex and Buster reroll use the same distance rule; ordinary fuse keeps its documented relative weights. Every actual manual move of a connected source refreshes each affected unlocked Hex once; locked Hexes remain unchanged. Element placement tools change geometry without implicitly spending generation calls.
