developer docs

brandmint api

integrate voice-matched content generation into any agent, cli, or workflow. extract voice profiles, generate posts, check quality, manage drafts, search post history, and publish to linkedin and x programmatically.

introduction

brandmint is a content engine that learns how you write. it reads your linkedin and x (twitter) posts, builds an 8-dimension voice profile (tone, hooks, rhythm, formatting, vocabulary, themes, structures, engagement patterns), and generates new posts that match your authentic voice.

the api exposes this as discrete endpoints. an agent can extract your voice, generate a post on a topic, check it for ai tells, manage drafts through a review pipeline, schedule posts, and publish to connected platforms, all without touching a browser. the mcp server wraps these same capabilities for claude code and other mcp-compatible clients.

authentication

all api requests require a key. create one in the brandmint app under settings → api keys, or via the key management endpoint.

pass the key in either header:

Authorization: Bearer bm_your_key_here // or X-API-Key: bm_your_key_here

keys are scoped to a single user account. all data returned is scoped to that user's posts, profile, and memories.

create a key (api)

if you're already authenticated via session (logged in), you can create keys programmatically:

POST /api/v1/keys { "name": "claude code" } { "id": "uuid", "key": "bm_a1b2c3...", "key_prefix": "bm_a1b2c3", "message": "Save this key now. It will not be shown again." }

quickstart

generate a post in your voice with one request. auto-extracts your voice, generates with a 3-step quality pipeline, and returns both the post and the reusable voice profile:

curl -X POST https://brandmint.app/api/v1/posts/generate \
  -H "Authorization: Bearer bm_your_key" \
  -H "Content-Type: application/json" \
  -d '{"angle": "why most content strategies fail"}'

response:

{ "post": "Most content strategies fail because ...", "voice_profile": "Tone: direct, contrarian ..." }
the first request takes longer because it extracts your voice from your posts. pass the returned voice_profile back on subsequent calls to skip extraction and speed things up.

error responses

all errors return a json object with an error field and an appropriate http status code.

{ "error": "description of what went wrong" }

status codes

  • 400 -missing or invalid parameters (e.g. no topic, no post text)
  • 401 -missing or invalid api key
  • 429 -daily rate limit exceeded
  • 500 -server error (database failure, upstream api error)

common error messages:

// missing api key 401 { "error": "API key required" } // invalid key 401 { "error": "Invalid API key" } // rate limited 429 { "error": "Rate limit exceeded. Try again tomorrow." } // missing required field 400 { "error": "topic is required" }

voice extraction

analyzes your last 15 posts to extract your authentic voice across 8 dimensions: tone, hooks, rhythm, formatting, vocabulary, themes, structures, and engagement patterns. voice extraction is platform-scoped: if you have posts on both linkedin and x, each platform gets its own voice profile.

POST /api/v1/voice { "voice_profile": "Tone: direct, contrarian ..." }

the voice profile is a text block. pass it to /posts/generate to skip re-extraction and speed up generation.

generate post

writes a post in your voice using the full 3-step pipeline: voice extraction (platform-scoped), generation with format-specific rules, and quality check (ai-tell scan + anti-pattern enforcement). supports linkedin and x.

POST /api/v1/posts/generate
{
  "angle": "why ops pilots fail",  // use the hook from generate_ideas for best results
  "objective": "authority",  // optional: authority | engagement | dms | lead-magnet
  "platform": "linkedin",  // optional: linkedin | x (scopes voice extraction)
  "format": "linkedin-post",  // optional: format key for structure rules
  "angle_rationale": "contrarian take on...",  // optional: from ideas "Why It Works"
  "subtopics": ["hiring", "retention"],  // optional: from ideas "Subtopics to Cover"
  "voice_profile": "..."  // optional: pass cached profile to skip extraction
}

→ { "post": "...", "voice_profile": "..." }

best results: call /posts/ideas first, pick an idea, then pass its hook as angle, its rationale as angle_rationale, and its subtopics as subtopics. this mirrors the app's swipe-to-draft flow. topic is accepted as an alias for angle for backwards compatibility. if platform is set, voice extraction only analyzes posts from that platform. format loads format-specific hooks, structures, and closes (defaults to linkedin-post).

quality check

scans a draft for ai tells and structural anti-patterns, then returns a cleaned version with violations removed.

POST /api/v1/posts/check
{
  "draft": "your post text here...",
  "voice_profile": "..."  // optional
}

→ { "cleaned_post": "..." }

catches common ai writing patterns including filler phrases, false dichotomies, rhetorical-question openers, and engagement bait.

generate ideas

turns a seed topic into 4-6 post ideas. each includes a hook, content gap analysis, subtopics, and cta. grounded in your recent posts so it avoids repeating what you've already covered.

POST /api/v1/posts/ideas { "seed": "remote work culture", "objective": "authority" // authority | engagement | dms | lead-magnet (default: authority) } { "ideas": "..." }

post analytics

aggregate your post data with counts, sums, and grouping. use for performance analysis: total reactions, posts per month, engagement by hook type, top-performing structures.

POST /api/v1/posts/analytics { "aggregate_by": { "count": ["Count"], "total_reactions": ["Sum", "reactions"] }, "group_by": ["hook_type"], // platform | hook_type | structure_type | media_type | month "days_ago": 90, // optional: 0 = all time "min_reactions": 10, // optional "limit": 20 // optional: max groups (default: 50) } { "analytics": "..." }

aggregation functions

  • ["Count"] - count posts
  • ["Sum", "reactions"] - sum a numeric field (reactions, comments, views)

for averages, request both sum and count, then compute client-side.

content strategy

analyzes your content performance and produces a data-backed strategy. covers: what's working (by hook type, structure, media format), what's underperforming, and specific recommendations with cited data points.

POST /api/v1/strategy { "timeframe": "30_days" } // 30_days | 60_days | 90_days (default: 30_days) { "strategy": "..." }

profile

read your creator profile (name, audience, content themes, post stats, saved memories) or update individual fields.

GET /api/v1/profile { "profile": "..." } PATCH /api/v1/profile { "name": "Jane Smith", // optional "headline": "coo at acme", // optional "bio": "ops nerd building systems...", // optional "audience": "technical founders", // optional "linkedin_url": "https://linkedin.com/in/janesmith", // optional "content_themes": ["operations", "ai adoption"] // optional }

drafts

full crud for drafts. drafts progress through a review pipeline: generated (pending) → approved (kept, shown in calendar) or rejected (passed, hidden). status is derived from the swipe_decisions table, not a column on drafts.

list drafts

GET /api/drafts // returns approved drafts (for calendar) GET /api/drafts?filter=pending // returns drafts awaiting review (no swipe decision yet)

create a draft

POST /api/drafts { "post_text": "your final post...", "platform": "linkedin", // linkedin | x "user_intent": "ops pilots" } { "id": "uuid", "post_text": "...", "share_slug": "abc123", ... }

create via public api / mcp

drafts created via the public api or mcp save_draft tool are auto-approved (a "kept" swipe decision is inserted) so they appear in the calendar immediately.

POST /api/v1/drafts { "post_text": "your final post...", "topic": "ops pilots", // optional: composes a structured intent "objective": "authority", // optional: authority | engagement | dms | lead-magnet "platform": "linkedin", // optional, default: linkedin "voice_profile": "..." // optional: persisted for editor context }

update a draft

PATCH /api/drafts { "id": "draft-uuid", "post_text": "edited content...", "is_scheduled": true, "scheduled_at": "2026-06-10T09:00:00Z" }

delete a draft

DELETE /api/drafts { "id": "draft-uuid" } { "deleted": true }

draft generation

generates a post from an idea angle using sse streaming. extracts voice (or uses a cached profile), writes the post, and saves it to the drafts table. the stream keeps the connection alive during generation (typically 10-20 seconds).

POST /api/drafts/generate { "angle": "The best networking events aren't conferences", "angle_rationale": "contrarian take on networking...", // optional "subtopics": ["why conferences fail", "better alternatives"], // optional "draft_id": "uuid", // optional: update existing placeholder "voice_profile": "..." // optional: skip voice extraction }

returns an sse stream with status updates and a final result:

event: status data: {"text": "extracting voice..."} event: status data: {"text": "writing draft..."} event: status data: {"text": "saving..."} event: done data: {"post": "...", "voice_profile": "...", "draft_id": "uuid"}
if draft_id is provided, the endpoint updates the existing row instead of creating a new one. this supports the placeholder pattern where a draft row is created first with empty post_text, then filled by this endpoint.

swipe decisions

records keep/pass decisions on ideas and drafts. each decision stores a full snapshot of the item at swipe time, enabling preference learning. draft status (pending vs kept vs passed) is derived from this table via postgres views.

record a decision

POST /api/swipe-decisions { "item_type": "draft", // "idea" or "draft" "item_id": "uuid", // optional, null for ideas without a db row "decision": "kept", // "kept" or "passed" "reason": "wrong tone", // optional rejection reason "item_snapshot": { "body": "the post text...", "platform": "linkedin" } }

list decisions

GET /api/swipe-decisions // returns all decisions for the authenticated user GET /api/swipe-decisions?type=idea&decision=passed // filter by item type and/or decision

rejection reasons

the app uses per-phase reason sets:

  • ideas: wrong topic, weak angle, overdone, wrong audience, too broad, not now
  • drafts: wrong tone, weak hook, wrong structure, too pushy, needs editing, not now

memories

persistent facts minty learns about you: preferences, corrections, rules, audience details, tone notes. saved automatically during chat (via the update_user_memory tool) and visible in the app under /app/memory.

list memories

GET /api/memories // returns all active memories for the authenticated user

create a memory

POST /api/memories { "content": "prefers short punchy hooks, no rhetorical questions", "category": "preference" // preference | correction | rule | audience | tone | general }

deactivate a memory

PATCH /api/memories { "id": "uuid", "is_active": false }

knowledge base

upload reference documents (brand guides, case studies, positioning docs) that minty can search while drafting. content is chunked and embedded for semantic search via the search_knowledge_base tool.

list documents

GET /api/knowledge // returns all documents, newest first GET /api/knowledge?q=brand // search by title or content

upload a document

POST /api/knowledge { "title": "brand-voice-guide.md", "content": "# Brand Voice ...", "file_type": "text/markdown", "file_size": 4200 }
content is automatically chunked (2000 chars with 200 char overlap) and embedded via openai text-embedding-3-small. the chunks power the search_knowledge_base tool during chat.

delete a document

DELETE /api/knowledge { "id": "uuid" } // cascades to embedded chunks

media upload

upload images to supabase storage. returns a public url and an optional vision summary (gpt-4o mini describes the image in one sentence for llm context).

POST /api/media // multipart/form-data form fields: file: (binary) // jpeg, png, webp, gif. max 10mb context: "draft text..." // optional: improves vision summary skip_summary: "true" // optional: skip vision analysis { "url": "https://...supabase.co/storage/v1/object/public/media/...", "path": "user-id/1717849200000.jpg", "media_summary": "a comparison chart showing Q1 vs Q2 conversion rates" }

uploaded images can be attached to drafts in the editor. their vision summaries are passed to minty as media_context during chat, so she can see what the image contains without viewing it directly.

publish

publishes a draft to a connected social platform. the draft must have content, not be already published, and the user must have an active connection for the draft's platform.

POST /api/publish { "draft_id": "uuid" } { "published": true, "url": "https://linkedin.com/feed/update/...", "draft": { ... } }

error cases

  • 400 - draft_id missing, draft has no content, or no connection for platform
  • 404 - draft not found
  • 409 - draft already published
  • 401 - token expired (reconnect your account)

supported platforms: linkedin, instagram. token refresh is handled automatically when possible.

connections

manage social platform connections. connections store oauth tokens and are used by the publish endpoint.

list connections

GET /api/connections // returns active connections (tokens are never exposed) → [{ "id": "uuid", "provider": "linkedin", "account_name": "Jane Smith", "account_handle": "janesmith", "is_active": true, "connected_at": "2026-06-01T..." }]

start oauth flow

POST /api/connections { "provider": "linkedin" } { "auth_url": "https://www.linkedin.com/oauth/v2/authorization?..." }

redirect the user to the returned url. after consent, the callback stores the token and redirects back to the app.

disconnect

DELETE /api/connections { "id": "uuid" } { "disconnected": true }

content pillars

recurring themes that organize your content. pillars are discovered automatically during onboarding and can be created or updated manually. used by the generator to ground ideas in your established themes.

list pillars

GET /api/pillars // returns active (non-retired) pillars

create a pillar

POST /api/pillars { "name": "operations", "description": "how to run teams and systems at scale" }

update a pillar

PATCH /api/pillars { "id": "uuid", "name": "leadership ops" }

settings

read or update your account settings: name, audience, objective, timezone, content themes, and connected social handles.

get settings

GET /api/settings

update settings

PATCH /api/settings { "audience": "technical founders building b2b saas", "default_objective": "authority", "timezone": "Europe/Prague" }

rate limits

default: 1,000 requests per day per key. resets at midnight utc. exceeded limits return 429 Too Many Requests.

usage is tracked per key and visible in the app under settings → api keys (calls today / daily limit).

// error response when rate limited { "error": "Rate limit exceeded. Try again tomorrow." }

sse events

the chat and draft generation endpoints use server-sent events (sse) to stream responses. each event has a type and a json data payload.

chat endpoint

the chat endpoint accepts optional context fields for the editor:

POST /api/chat { "messages": [...], "conversation_id": "uuid", // optional: resume existing "draft_id": "uuid", // optional: injects draft context (text, voice, angle) "media_context": [{...}] // optional: attached image summaries }

when draft_id is present, the system prompt includes an <active_draft> block with the draft text, platform, original angle, voice snapshot from generation, and any attached media summaries.

chat events

  • session -sent once at the start. contains conversation_id.
  • text -streaming text chunks. {"text": "..."}
  • status -progress updates. {\"text\": \"analyzing your voice...\"}
  • tool_output -structured results for client-side rendering (e.g. idea cards, strategy data). {\"content\": \"...\"}
  • done -stream complete. contains conversation_id and usage (tokens, cost).
  • error -something failed. {"error": "message"}

draft generation events (/api/drafts/generate)

  • status -progress updates: "extracting voice...", "writing draft...", "saving..."
  • done -generation complete. contains post, voice_profile, and draft_id.
  • error -generation failed. {"message": "..."}

consuming sse in javascript

const res = await fetch("/api/drafts/generate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ angle: "your topic" }), }); const reader = res.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\\n"); buffer = lines.pop(); for (const line of lines) { if (line.startsWith("data: ")) { const data = JSON.parse(line.slice(6)); // handle data based on preceding event: line } } }

mcp server

brandmint exposes a hosted mcp endpoint. paste the url into any mcp-compatible client and all tools appear. no npm install, no local server.

claude code

{ "mcpServers": { "brandmint": { "type": "url", "url": "https://brandmint.app/api/mcp", "headers": { "Authorization": "Bearer bm_your_key" } } } }

cursor / windsurf

same endpoint and auth. add it in your mcp settings with the url https://brandmint.app/api/mcp and your api key as the bearer token.

once connected, all 28 tools appear automatically in your client. the server includes dynamic instructions that guide your mcp client through the recommended workflow. new users get onboarding steps; fully set up users get the standard ideas-first flow.

onboarding

the mcp server detects your setup state on every connection. if posts, audience, or social connections are missing, the server instructions tell your client to resolve them first. call get_setup_status to check what's configured and what's missing.

get_setup_status() { "is_new_user": true, "post_count": 0, "has_audience": false, "linkedin_connected": false, "missing_steps": [ "sync_posts: no posts imported yet...", "set_audience: no target audience defined..." ] }

try these prompts

  • "generate post ideas about remote hiring"
  • "write a linkedin post about why most ops pilots fail"
  • "extract my voice profile from my recent posts"
  • "check this draft for ai tells" (paste your text)
  • "how did my posts perform this month?"
  • "save this as a draft and publish it to linkedin"

what happens under the hood

generate_post runs the same 3-step pipeline as the app: voice extraction (platform-scoped), generation (format-aware), and quality check (ai-tell scan + anti-pattern enforcement). the returned post is already cleaned. save_draft auto-approves the draft so it appears in your calendar immediately, and persists the voice profile for editor context.

example workflows

chain tools together for end-to-end content workflows. these examples work via the mcp server or rest api.

idea to published post

the recommended flow: generate ideas, pick one, write it, save, and publish. quality check is built into generate_post (no separate step needed).

1. generate_ideas(seed: "remote hiring") → returns 4-6 ideas, each with hook, rationale, subtopics 2. swipe(item_type: "idea", item_id: id, decision: "kept") → approve the idea (pass the rest with decision: "passed") 3. generate_post(angle: "the best hires don't come from job boards", angle_rationale: "...", subtopics: [...], platform: "linkedin") → returns { post, voice_profile } (already quality-checked) 4. save_draft(post_text: post, topic: "remote hiring", voice_profile: voice_profile) → auto-approved, appears in calendar 5. publish_draft(draft_id: id) → live on linkedin

performance review

analyze what's working and generate ideas to double down on top-performing themes.

1. aggregate_posts(aggregate_by: {"count": ["Count"], "reactions": ["Sum", "reactions"]}, group_by: ["hook_type"], days_ago: 90) → see which hook types drive engagement 2. content_strategy(timeframe: "90_days") → get data-backed recommendations 3. generate_ideas(seed: "top-performing theme from step 1", objective: "engagement") → ideas grounded in what already works

voice-first batch

extract voice once, then reuse it across multiple generations to skip the extraction latency.

1. extract_voice() → save the returned voice_profile string 2. generate_post(angle: "topic A", voice_profile: saved) 3. generate_post(angle: "topic B", voice_profile: saved) 4. generate_post(angle: "topic C", voice_profile: saved) → 3 posts, voice extracted only once

carousels

create, read, update, and delete carousels. each carousel contains a slides array (jsonb), a visual style, aspect ratio, and optional brand kit link.

list carousels

GET /api/v1/carousels
-H "Authorization: Bearer bm_your_key"

→ [
  {
    "id": "uuid",
    "title": "5 hiring mistakes",
    "aspect": "1:1",
    "style_id": "bold",
    "slide_count": 8,
    "slides": [{ ... }],  // first slide only (preview)
    "created_at": "...",
    "updated_at": "..."
  }
]

the list response returns only the first slide per carousel (for thumbnail previews) plus a slide_count. use ?id=uuid to fetch all slides.

get a single carousel

GET /api/v1/carousels?id=uuid { "id": "...", "slides": [...], "brand_kit_id": "...", ... }

create a carousel

POST /api/v1/carousels { "title": "5 hiring mistakes", "slides": [...], "aspect": "1:1", // 1:1 | 4:5 | 3:4 | 9:16 "style_id": "bold", // see /carousels/styles "brand_kit_id": "uuid" // optional } { "id": "uuid", ... }

update a carousel

PATCH /api/v1/carousels { "id": "carousel-uuid", "title": "updated title", "slides": [...] // full slides array }

delete a carousel

DELETE /api/v1/carousels { "id": "carousel-uuid" } { "deleted": true }

writable fields

  • title - carousel name
  • slides - full slides jsonb array (each slide has elements, background, layout)
  • aspect - canvas ratio: 1:1, 4:5, 3:4, 9:16
  • style_id - visual style key (see styles endpoint below)
  • framework - content framework used to generate slides
  • source_type - how the carousel was created
  • brand_kit_id - FK to a brand kit
  • draft_id - FK to the draft this carousel is attached to
  • thumbnail_url - preview image url

webhooks

subscribe to events to keep external systems in sync. webhook configuration is coming soon.

  • draft.saved -a draft was saved via the api or app
  • post.scheduled -a draft was placed on the calendar
  • post.published -a post went live via a connector
  • voice.extracted -a voice profile was generated