Developers

Run BetterSocials from your own agent

API version 2026-08-16 · machine-readable manifest at /api/meta

What this is

One API key runs the whole product. An AI agent — Claude, Claude Code, Codex, anything that speaks JSON over HTTP — can read the unified inbox and reply to comments and DMs, create, caption, schedule and publish posts, manage comment-keyword automations and calendar notes, read analytics, and do full video edits in Studio. These are the same routes the app itself calls, so every validation, credit meter and guard applies to an agent exactly as it applies to a person clicking.

Two things stay human on purpose: video Export (rendering runs in the browser, so a person presses the button after checking the work), and anything that touches money, account settings or channel connections — those never accept a key at all.

Use it in Claude or ChatGPT

If you just want your AI assistant to run your BetterSocials, you can skip the rest of this page. There is no key to copy and nothing to install: paste one address into the app you already use, then sign in when it asks.

https://bettersocials.ai/api/mcp

Claude (claude.ai, desktop or mobile): Settings → Connectors → Add custom connector. Name it BetterSocials, paste the address above, then Add and Connect. Claude opens a BetterSocials sign-in, you approve, and the tools show up in your next chat. Custom connectors need a paid Claude plan.

ChatGPT: Settings → Developer mode (under Plugins) to turn it on, then close Settings and open Plugins in the left sidebar. The plugin is made there, not on the settings screen. Hit the plus button, name it BetterSocials, leave Connection on Server URL and paste the address above, leave Authentication on OAuth, tick “I understand and want to continue” (Create stays disabled until you do), and Create. ChatGPT walks you through the same sign-in and approval, and you switch it on per chat from the composer’s plus menu under Developer mode. Developer mode needs a paid ChatGPT plan on the web, and write actions ask you to confirm each time.

Both doors ask for a name and neither enforces one. Use BetterSocials in both anyway: the name is how you aim a prompt at these tools, and every example here says “use the BetterSocials tools”.

The connection acts with your workspace’s normal limits, and Settings → Connected apps in BetterSocials disconnects it in one click.

Built so a leaked key stays boring

Every key is scoped to one workspace, stored as a SHA-256 hash, revocable in one click, and rate-limited in three tiers: reads (600 per 5 minutes), writes (120), and sends — anything that leaves the app onto a real social account — at 30. Tripped limits page our security channel. Key management, billing, account settings and channel connect/disconnect stay session-only, so a leaked key can never mint more keys, spend money, or deprovision an account. Sends run through the same claim locks and failure ledger the app uses — an agent cannot send around a guard. Repeated invalid keys get throttled per network and page us too.

Stay current, automatically

Every response from the agent surface carries a docs object linking to https://bettersocials.ai/api/meta — the live manifest of every endpoint, rule and limit. Point your agent at it once and it always knows what it can do, including things we shipped after you wrote your integration. The MCP server exposes the same thing as the get_capabilities tool.

Authentication

Mint an API key in Settings → API keys. Keys are shown once, stored as a SHA-256 hash, capped at five active per workspace, and scoped to that workspace — an id in a URL never reaches across. Key management itself stays in the app, so a leaked key can never mint or revoke keys.

curl -s https://bettersocials.ai/api/studio/editor/projects \
  -H "Authorization: Bearer br_YOUR_KEY"

Connect over MCP

The hosted MCP server wraps the same contract as tools, over Streamable HTTP. One command in Claude Code:

claude mcp add --transport http bettersocials https://bettersocials.ai/api/mcp \
  --header "Authorization: Bearer br_YOUR_KEY"

Tools cover the same surface: get_inbox, get_thread, send_reply, draft_reply, create_post, generate_captions, list_queues, list_keywords, create_keyword, get_analytics, list_connections, the Studio project tools, create_upload_url, and more — get_capabilities always lists the current set.

The workflow

  1. Read. GET the project. The whole edit is one JSON document — clips, cuts, captions, overlays, effects, sounds. Keep the updatedAt you read.
  2. Modify. Change the document in memory. Cuts are kept source ranges; overlay, effect and sound times are output seconds.
  3. Write. PATCH the whole document back with baseUpdatedAt. A stale base is a 409 — re-read, re-apply, retry. Identical content is a free no-op.
  4. Watch. Any open editor tab applies your patch live within about a second, and the creator can undo it with ⌘Z like their own edit.

Getting footage in

Media bytes never ride the document. Ask POST /api/studio/editor/media/upload for a signed URL, PUT the file straight to storage, then reference the returned publicUrl as a clip or overlay sourceUrl. The open editor fetches it once into its local vault. Supply analysis (word timings plus measured speech bounds) with a clip and the editor never re-charges transcription.

Endpoints

MethodPathWhat
GET/api/metaThis manifest — every capability, limit and rule, as JSON. No auth needed.
GET/api/studio/editor/projectsList the workspace’s editor projects (summaries, newest first).
POST/api/studio/editor/projectsCreate a project. Optionally seed the full document.
{ title?, aspect?: 'vertical' | 'wide', doc? }
GET/api/studio/editor/projects/:idThe full project including its document. `project.updatedAt` is what you send back as `baseUpdatedAt`.
PATCH/api/studio/editor/projects/:idSave the project. The doc REPLACES the stored one (omitted fields are RESET) — always GET, modify, PATCH the whole thing. The response carries `warnings` naming any field sanitization dropped or clamped; read them, or you will report an edit that never happened. For small changes prefer /edits below.
{ title?, doc?, durationSec?, poster?, baseUpdatedAt? }
POST/api/studio/editor/projects/:id/editsTargeted edit operations, applied server-side — no doc round-trip, no risk to the rest of the document. This is the whole editing surface: captions and their look, title card, timing cuts, filler removal, speed, texts, overlays, effects, sounds, green screen, framing. Every op reports applied-or-why-not. Ops: add_clip { url (a publicUrl from import_media/create_upload_url/list_media), name?, durationSec? } — footage itself; pair with transcribe_clip for words · set_captions { on, styleId? } · set_caption_style { styleId?, adjustments? { fontId, caseMode uppercase|lowercase|none, baseColor/activeColor/strokeColor #rrggbb, strokeRatio, sizeScale, baselineRatio (vertical position), offsetXRatio, maxWords }, reset? } · set_title_card { text|null, color?, endSec? (how long it stays up), casing?, background?, fontId?, offsetY?, scale? } · set_speed { speed 0.5–2 } · set_clip_speed { clipIndex, speed|null } · set_pacing { tight|natural|relaxed } · set_tighten_pauses { on, thresholdSec?, targetSec? } · set_soft_cut { on } · set_video_transform { x?, y?, scale? | reset } · set_green_screen { on, background { kind color|media|none, color?, url? }, windows? [{startSec,endSec}]|null } · cut_source_range { clipIndex, startSec, endSec — SOURCE seconds; word timestamps in analysis.words are source seconds (+ asrOffset) } · run_filler_removal { strength? light|standard|aggressive } — the editor’s own um/uh/stutter pass, server-side · add_text / update_text / remove_text { text, presetId?, startSec?, endSec?, x?, y?, sizeRatio?, fontId?, casing?, color?, strokeColor?, strokeRatio?, bgColor?, bgOpacity?, shadow?, animation none|pop|fade|slide-up|typewriter } · add_overlay / update_overlay / remove_overlay { url (public image/GIF/video), kind?, startSec?, endSec?, x?, y?, w?, opacity?, enter pop|fade|none, corner?, shadow?, muted?, layer under|over, crop? } · add_effect / update_effect / remove_effect { kind punch|zoom-in|zoom-out|shake|flash, startSec, endSec?, intensity? } · add_sound / update_sound / remove_sound { sfxId (built-in kit — see capabilities.vocab.sfx) or url (audio file), startSec?, durationSec?, sourceOffsetSec?, gain 0–1.5, lane? }. Vocab (every styleId, fontId, presetId, sfxId): GET /api/meta → vocab, or the MCP get_capabilities result.
{ ops: [{ op, … }], baseUpdatedAt? }
POST/api/studio/editor/projects/:id/transcribeTranscribe one clip server-side (MCP: transcribe_clip) so word-level editing works over the API — after this, cuts, filler removal and captions all have words. The clip’s sourceUrl must live in BetterSocials storage (import_media / upload / composer). Spends the same credit as in-editor captioning; already-transcribed clips return free; silent clips are refunded.
{ clipIndex?: number, force?: boolean }
DELETE/api/studio/editor/projects/:idDelete the project. Its synced media is swept ~48h later.
POST/api/studio/editor/media/uploadGet a signed URL to upload footage, images or audio straight to storage — then reference the returned publicUrl as a clip or overlay `sourceUrl`.
{ contentType, sizeBytes, filename? }
POST/api/studio/editor/media/importServer-side fetch of a PUBLIC media URL into the workspace bucket — for agents whose sandbox blocks a direct PUT to storage (egress allowlists). Drive/Dropbox share links are rewritten to direct downloads when possible. Max 500MB; larger files go through the composer.
{ url, filename? }
GET/api/studio/editor/media/uploadsRecent media in the workspace bucket (agent uploads/imports + files the creator dropped into the composer; last 30 days, newest first) — the human-handoff when an agent cannot move bytes at all.
POST/api/studio/editor/mediaThe ingest proxy the open editor uses to fetch a doc-declared external sourceUrl. You normally never call it yourself.
{ url }
GET/api/studio/shortsList the workspace’s short-form scripts (?status= filter).
POST/api/studio/shortsCreate a blank short script.
{ title? }
POST/api/studio/shorts/generateGenerate one complete short from the Creator Brief. Metered exactly like the in-app button.
{ brief?: string }
GET/api/studio/shorts/:idThe full short (beats, captions, title cards, strategy, media).
PATCH/api/studio/shorts/:idUpdate a short — same validated field set the editor uses; send baseUpdatedAt.
DELETE/api/studio/shorts/:idDelete a short script.
GET/api/studio/briefThe Creator Brief.
PUT/api/studio/briefReplace the Creator Brief (normalized and capped).
GET/api/studio/hooksThe hook library generation reads — house and earned patterns, evidence, tracked creators.
POST/api/mcpHosted MCP server (Streamable HTTP) wrapping this same contract — connect Claude Code, Claude Desktop or any MCP client with your API key as a Bearer header.
GET/api/inboxThe unified inbox — comments + DMs across every channel, one thread per contact. Filters: channels (csv), type (comment|message), status (pending|sent|active|archived|spam|all), leads=1, q= search, limit/cursor paging.
GET/api/inbox/:id/threadOne contact’s merged conversation timeline plus reply_targets (what a reply can anchor to). ?item_type=comment|message says what the id points at.
POST/api/inbox/:id/statusTriage a whole contact thread.
{ status: 'pending'|'archived'|'spam'|'replied_externally', item_type? }
GET/api/inbox/unseenThe inbox badge count — actionable inbound newer than the last inbox view. Poll this cheaply instead of the full inbox.
POST/api/replies/:idSEND a reply on the creator’s real account — public comment reply or DM. Claim-locked, account-resolved, failure-ledgered; bearer calls draw the send tier.
{ text, item_type?: 'comment'|'message', channel?: 'comment'|'dm' }
POST/api/replies/:id/draftAI reply draft in the creator’s voice (?item_type=). Spends 1 credit; templates when credits run out. Nothing sends.
GET/api/postsPosts in a window (?from=&to= ISO) or every draft (?drafts=1).
POST/api/postsCompose a post: draft:true saves, queue:true books the next queue slot, schedule{date,time,timezone} books a time, publishNow:true publishes immediately (send tier). Media URLs must live in BetterSocials’s own bucket.
{ postType, media?, thumbnailUrl?, youtubeTitle?, baseText?, transcript?, captions?, targetConnectionIds, draft?|queue?|schedule?|publishNow? }
GET/api/posts/:idPost detail with per-platform results and links.
PATCH/api/posts/:idAct on a post by action: saveDraft, scheduleDraft, queueDraft, publishDraft (send tier), cancel, reschedule, edit, editCaptions, retry / retryLeg (send tier).
{ action, schedule?, queueId?, connectionId?, captions?, …content fields }
DELETE/api/posts/:idDelete a DRAFT post.
POST/api/posts/captionsGenerate per-channel captions (+ YouTube Shorts title) in the creator’s voice. Spends credits per channel; can weave a comment-keyword CTA.
{ postType, targetConnectionIds, transcript?, baseText?, youtubeTitle?, includeKeyword?, keywordMode?, keywordId? }
POST/api/posts/transcribeTranscribe an uploaded video (own-bucket URLs only; 60/day per user).
{ mediaUrl }
GET | POST/api/calendar/notesCalendar notes in a day window (?from=&to= YYYY-MM-DD) / create one ({ day, title, body?, color? }).
PATCH | DELETE/api/calendar/notes/:idEdit or delete a calendar note.
GET | POST | PATCH | DELETE/api/keywordsThe comment→DM automation keywords. Create needs keyword + guide_url + ≥1 dm_templates + ≥1 public_reply_templates; PATCH/DELETE take { id, … }. Every keyword needs its static fallbacks — the engine refuses one that could silently send nothing.
GET | POST | PUT | PATCH/api/posting-queuePosting queues: list (+ queued posts + timezone), create, rename/recolor/reslot, and remove/reorder/swap/moveToQueue/deleteQueue actions.
GET | POST | PATCH | DELETE/api/account-groupsNamed account sets used across create/keywords/analytics.
GET | POST | PATCH | DELETE/api/ai-rulesThe creator’s always/never AI voice rules injected into every draft.
GET/api/analyticsThe growth read model: follower series, per-day earned metrics, posts with outlier scores, keyword conversions. ?range=1d|7d|1m|90d|1y|all. Pro+ plans.
GET/api/connectionsConnected channels (id, platform, handle, status). READ-ONLY by key — connecting, editing and disconnecting stay in the app.

The rules that keep collaboration safe

  • Auth: `Authorization: Bearer br_…` — mint keys in Settings → API keys (shown once, 5 active per workspace, SHA-256 at rest). Every workspace has API access. A presented key is judged alone; key management stays in-session.
  • MCP without a key: claude.ai and ChatGPT connect through OAuth. Paste the MCP endpoint into their connector settings and sign in when asked. The granted token has the same authority and limits as an API key, and Settings → Connected apps disconnects it instantly.
  • The workspace is resolved from the key. Ids in URLs never cross workspaces.
  • Every bearer call is rate limited per workspace in three tiers: reads 600/5min, writes 120/5min, sends 30/5min — MCP tools included. A 429 carries Retry-After — back off and retry; tripped limits alert our security channel. Repeated invalid keys are throttled per network.
  • Sends are sacred: send_reply / publishNow / publishDraft / retry act on the creator’s REAL social accounts. Do them only on explicit instruction or standing rules the creator set. Every send passes the same claim locks, account resolution and failure ledger the app itself uses.
  • Session-only forever: key management, billing, account/profile, workspace management, and connecting/disconnecting channels. A leaked key can never mint keys, spend money, or deprovision an account.
  • AI generation (drafts, captions) spends the workspace’s credits exactly like the in-app buttons — an agent can never generate more cheaply than a human.
  • PATCH replaces the whole document. A partial doc is a valid doc with everything else missing — always GET → modify → PATCH. For a small change (captions, title card, speed, a cut) use /projects/:id/edits (MCP: edit_project) instead: no round-trip, and every op answers applied-or-why-not.
  • Optimistic concurrency: send `baseUpdatedAt` (the `updatedAt` you read). Stale = 409 → re-GET, re-apply, re-PATCH.
  • Identical-content PATCHes are no-ops — retries are free and never fight an open editor tab.
  • Docs are sanitized both directions: unknown fields drop, numbers clamp. Diff the response against what you sent.
  • Media bytes never ride the doc. Upload via /media/upload (or host them yourself) and reference the URL as `sourceUrl` on clips, overlays, sounds and green-screen backgrounds; any open editor ingests it once into its local vault.
  • Sandboxed and cannot PUT to storage? Never a dead end: POST /media/import fetches a public URL server-side into the bucket (MCP: import_media), or the creator drops the file into the composer and GET /media/uploads (MCP: list_media) finds it.
  • Supply `analysis` (ASR words + measured speech bounds) with a clip and the editor never re-charges transcription.
  • Every successful write pings open editor tabs live (≈1s). Agent edits land as undo steps — ⌘Z works on them.
  • Export stays human: rendering is client-side by design, so a person presses Export in the tab. Everything up to that button is yours.
  • Uploaded media not referenced by any project document within 48 hours is swept.

Limits

projectsPerWorkspace100
docBytes3500000
clipsPerProject24
overlays60
effects60
sounds60
texts60
wordsPerClip22000
cutsPerClip240 short-form, scaling with footage to 1200
maxSeconds7200

Open to every workspace

Every BetterSocials workspace has API access — sign up, mint a key in Settings → API keys, and go. Something missing that your agent needs? Tell us through the in-app support chat — gaps in this surface get closed fast, and the manifest at /api/metais how you’ll see them land.