Skip to content
ShapelessAI← Back
Interfaces

API reference

Every route an API key opens, the scope each one needs, and the routes that refuse a key on purpose.

Verified
View as Markdown

Everything an agent can reach over HTTPS, and the scope each route needs. The studio in a browser rides a signed session cookie; an agent outside that browser rides an API key - the same account, the same routes, a second credential.

curl -H "Authorization: Bearer slk_..." https://shapelessai.com/api/me

Minting a key, the three scopes and the refusal codes are on Authentication. Putting a post out is on Posts. This page is the whole surface.

Machine-readable

curl -s https://shapelessai.com/api/openapi.json

OpenAPI 3.1 for every key-accepting route, no auth. Point a generator at it rather than reading this page, if that is what you are doing. The rendered operation list is at the bottom of this page.

The same spec is a ready Postman collection in the public ShapelessAI API workspace (team profile: ShapelessAI on Postman). Fork it, set SHAPELESS_API_KEY to your key, and send GET /api/me.

It is also listed on RapidAPI as ShapelessAI on RapidAPI, all 119 operations. Through RapidAPI's gateway send your X-RapidAPI-Key and also Authorization: Bearer slk_...; the gateway passes the Bearer key through, so calls run on your own ShapelessAI account and plan.

Paging

The thread lists - GET /api/conversations and GET /api/v1/jobs - answer 200 rows per page, newest first, alongside a nextCursor. Non-null means older rows exist: send it back verbatim as ?before=<nextCursor> for the next page, and keep going until it is null. The cursor is the last row's sort key (<updatedAtISO>~<id>), so a thread touched while you page is never skipped or served twice; anything else is a 400. ?origin= on the jobs list filters the page it answers, so a page can carry fewer than 200 jobs and still have a cursor - follow the cursor, not the count.

Routes that accept a key

Every row is enforced in the codebase by src/server/auth/route-scopes.test.ts: if a route and this table disagree, the build fails.

Identity and accounts

MethodPathScopePurpose
GET/api/mereadWho am I, plan, credit balance, and next: up to three moves worth making on this account now (connect, post, make the image, hand over a goal), the skill at /skill.md and the docs.
GET/api/me/emailreadWhether lifecycle email (posts ready, weekly summary) is on, and the last emails we sent.
PUT/api/me/emailwrite`{ lifecycle: true
GET/api/connectionsreadConnected social accounts (identity only, no tokens), plus connect: one clickable link per platform with no live account, and reconnect on an expired one.
POST/api/connections/linkwrite{platform} -> the one URL the human opens to connect an account there. 15 minutes, signed in as this account only; the platform's OAuth follows in the browser.
DELETE/api/connections?id=publishDisconnect an account. Ends our ability to publish there.
PATCH/api/connections/[id]write{groupId} or null: move an account into a group.
GET/api/connections/[id]/tiktok-creator-inforeadTikTok's creator info for that connection (privacy levels, limits). Read it before every TikTok post; TikTok forbids caching it.
POST/api/connections/trackedwrite{platform, handle, followers?}: track an account by handle, no OAuth, never a publish target.
GET/POST/PATCH/api/groupsread / writeAccount groups: list, create {name, color?}, reorder {orderedIds}.
PATCH/DELETE/api/groups/[id]writeRename or recolor; delete (its accounts become ungrouped).
GET/api/capabilitiesnoneEverything Shapeless can do, live or planned, as JSON - the same list as Capabilities.
POST/api/capabilities/[id]/wantany, or none with {email} / {webhookUrl}Tell me when a planned capability ships.
GET/api/ad-accountsreadConnected ad accounts (identity only, no tokens) + whether connecting one is configured.
DELETE/api/ad-accounts?id=publishUnlink an ad account. Costs publish: it ends our ability to spend.

Chat

MethodPathScopePurpose
POST/api/studiowriteOne agent turn, NDJSON stream of StudioEvents.
GET/api/studio/tailreadRe-attach to a detached run's event log.
POST/api/studio/stopwriteStop a detached run.
PUT/api/studio/attachments/[filename]writeUpload a chat attachment, returns its mediaKey.
GET/api/conversationsreadThread list, one page (see Paging). ?origin=chat,agent,job,identity keeps only those provenances (identity: started beside the graph on Shapeless Graph), ?q= searches titles, ?strategyId= narrows to one agent's wakes, ?limit= shrinks the page. unread counts threads that finished since they were last opened.
GET/api/conversations/[id]readOne transcript, with each post artifact's queue state. Reading it marks the thread opened.
PATCH/DELETE/api/conversations/[id]writeRename or delete a thread.
POST/api/conversations/[id]/openedwriteMark a thread opened without reading it (clears its unread state).
GET/api/conversations/[id]/panelreadThe panel beside the chat on the Ask page, a read-only document of tabs the browser and a terminal share: {conversationId, panel: {rev, items, active?, suggestions}, text}, text one plain line per tab. ?after=<rev>&wait=<seconds> holds up to 25 s until the panel changes. Reading it does not mark the thread opened.
POST/api/conversations/[id]/panelwriteChange the panel: {ops: [...]} (open, update, show, close, pick, suggest, and edit {id, node, patch {text?, sub?, media?}}, the person's hand edit of one flow node; at most 50), folded in order through the reducer the browser uses. A bad op is a 400 naming which and why.
GET/api/conversations/current/panelreadThe same, for the thread the account opened last (the chat most likely open in the browser), with its conversationId.

Jobs

The durable alternative to stream plumbing. A job is a conversation whose ids the server mints: you send a goal, get a 202 with the job's id, and the run keeps going whether or not you stay connected. Poll the job for its transcript and outputs, or tail the live stream. Posting another message to a job builds the conversation history server-side from the stored transcript, so sending "continue" to a failed or stuck job resumes it.

MethodPathScopePurpose
POST/api/v1/jobswriteStart a job: {goal, label?, budgetUsd?, timezone?, attachments?, characters?} returns 202 {id, title, status}.
GET/api/v1/jobsreadList jobs with liveness, one page (see Paging). ?origin=job|chat|agent|identity filters by who started the thread.
GET/api/v1/jobs/[id]readOne job: enriched transcript, status/live, and an outputs summary (posts with queue state, media).
POST/api/v1/jobs/[id]/messageswriteAnother turn on the job: {text, budgetUsd?, timezone?, attachments?} returns 202 {id, status}.
POST/api/v1/jobs/[id]/runwriteRun the job's newest proposed plan, as the Run button would: {budgetUsd?} returns 202 {id, status}. Deliverables land as proposed drafts.

A running job streams into the same event log chat uses: follow it with GET /api/studio/tail?conversationId=<id>, stop it with POST /api/studio/stop. The label becomes the job's title; without one the goal's first line is. Refusals are the plan turn's: 402 when the month's free credits are spent, 429 when planning too fast, 400 for an attachment the account cannot use.

Attaching files

attachments puts files on the message itself - the same thing the web composer's paperclip does, and the agent sees them: an image's pixels are inlined for that turn, video and PDF go over by storage URI, and the media key is named in the text so it survives in history after the pixels are gone. Up to 6 per message; each entry needs a name and either text (inlined, 24k characters) or a mediaKey:

{
  "goal": "Does this thumbnail work for the launch post?",
  "attachments": [
    { "name": "thumb.png", "mediaKey": "chat-uploads/<account>/thumb-mt1z.png", "contentType": "image/png" },
    { "name": "notes.md", "text": "Launch is Thursday. Tone: plain, no hype." }
  ]
}

Get a mediaKey by uploading the bytes to PUT /api/studio/attachments/[filename] (10MB images, 30MB video/PDF; filenames are [A-Za-z0-9._-]). Any media key the account owns also works - a brand asset from workspace-assets/, media an earlier run produced - because the server copies it into this account's chat-uploads/ prefix before the turn, which is the only prefix the engine reads. A key belonging to another account is a 400 naming the file, not a run that dies halfway. A malformed entry is a 400 too: attachments are never dropped silently.

References

The creators, competitors and peers an account works from, the people we suggest, and a browse of what other workspaces picked (counts of 3 or more only). See MCP for the whole loop.

MethodPathScopePurpose
GET/api/v1/referencesreadThe sets with their members.
POST/api/v1/referenceswriteAdd a person: {handle, platform} or {url}, kind?: creator|competitor|peer, setId?, suggestionId?, personName?. Returns {member, created}.
DELETE/api/v1/references/[id]writeRemove a member (soft). ?reason=not_similar|not_relevant|low_quality|other.
GET/api/v1/references/suggestionsreadUndecided suggestions with reasons [{code, evidence}]; ?kind=, ?limit=1..20.
POST/api/v1/references/suggestions/[id]/decidewrite{decision: accept|reject, kind?, setId?}. Accept adds the member.
GET/api/v1/references/browseread?q=&platform=&limit=: your matches, picked counts, alsoPicked beside an exact handle, remaining today. 402 reference_browse_limit past the plan's day.

POST /api/v1/jobs takes referenceId (a member id) and mode (clone, format or inspiration, the default) to work from that person; POST /api/posts takes the same provenance as inspiration: {kind, id, mode?, memberId?}.

Shapeless Graph

The graph of who is who on social. Reads are free; a pull holds its budget on the wallet and is charged what it cost. See Identity for the controls and the stream.

MethodPathScopePurpose
GET/api/identity/searchread?q=&limit=: known people, accounts and organizations.
GET/api/identity/node/[id]readOne node: fields, edges, evidence, pulls, freshness.
GET/api/identity/graphread?anchor=: a node and its first ring.
GET/api/identity/homereadThe home graph in one read: what you track, your company, and the ring of every spoke.
GET/api/identity/peopleread?q=&sort=&category=&niche=&scope=&limit=.
GET/api/identity/facetsread?scope=: categories, niches, organizations with counts.
GET/api/identity/similarread?anchor=person:<id>&limit=&scope=: people like one person.
GET/api/identity/libraryread?anchor=&sort=&platform=&limit=&offset=: posts held.
GET/POST/DELETE/api/identity/trackedread / writeWhat you track; {node, role, note?} tracks, {node} stops.
GET/api/identity/pull-estimateread?node=&expansion=&since=&until=&sinceMonths=&maxItems=&budgetUsd=&force=: the price before the pull.
POST/api/identity/resolvewrite{seed, kind?, budgetUsd?, force?}, NDJSON. Spends credit.
POST/api/identity/expandwrite{node, expansion, budgetUsd?, since?, until?, sinceMonths?, maxItems?, force?}, NDJSON. Spends credit.
POST/api/identity/postwrite{url, person?, budgetUsd?, force?}: one post by its link, NDJSON. Spends credit.

Labels and an edge's confirmed or rejected status are written on the shared graph, so they stay with the founder's session for now and refuse a key.

Posts

MethodPathScopePurpose
GET/api/postsreadThe queue: proposed, scheduled, published. ?status= and ?limit= narrow it.
POST/api/postspublishPut a post on the rail: at scheduledAt, in the account's next free slot (queue: true), or now.
GET/api/posts/[id]readOne row's live state.
PATCH/DELETE/api/posts/[id]writeEdit or cancel a still-scheduled post (time, text, media, settings, firstComment, connectionId on the same platform).
GET/api/v1/instagram/audioreadSounds an Instagram account may attach to a Reel: ?q=, ?type=music|original_sound, ?connectionId=. Pass an id as POST /api/posts audio.audioId. An Instagram Login connection answers 409 {needsConnection, connect: {url}}.
GET/api/platformsnoneEvery platform the rail posts to: limits, media rules, settingsSchema, first-comment support.
GET/api/openapi.jsonnoneThe OpenAPI 3.1 document for every route in this file.
GET/PATCH/api/connections/[id]/queueread / writeThe account's posting slots and timezone, and the next free slot.
POST/api/posts/[id]/approvepublishApprove one proposal: proposed -> scheduled. A missing account answers 409 {needsConnection, connect: {url}} - hand connect.url to the human.
POST/api/posts/[id]/publishpublishPublish a queued post now.
POST/api/posts/[id]/mark-postedpublish"I posted it myself".
POST/api/posts/[id]/revisewriteRework a draft from feedback.
POST/api/posts/[id]/boostpublishPropose a paid boost of a published Meta post.
GET/api/posts/[id]/commentsreadThe thread under a published post, with each drafted reply.
POST/api/posts/[id]/commentspublishStage your own reply to one comment.
POST/api/posts/[id]/comments/refreshpublishRead the thread from the platform now.
POST/api/posts/resolvepublish (approve) / write (dismiss)Resolve a batch of proposals.
GET/api/inboxreadEverything waiting on a human decision.

POST /api/posts takes {connectionId, postText, mediaKeys?, documentTitle?, scheduledAt?, queue?, settings?, firstComment?}. scheduledAt omitted or in the past publishes now; queue: true takes the account's next free posting slot instead (GET /api/connections/[id]/queue shows the slots). settings follows the platform's settingsSchema from GET /api/platforms (TikTok privacyLevel, Reddit subreddit, YouTube privacyStatus); documentTitle is required on YouTube. firstComment is posted as the post's own first reply the moment it is live: LinkedIn (1250 chars), X (280), Bluesky (300); other platforms answer 422. A platform rule that refuses the post answers 422 with the rule in error. The Free plan posts ten a day, counted on the UTC day of the slot: the eleventh answers 402 {code: "free_daily_cap", limit, day, resetsAt}. Paid plans have no cap. The row's firstCommentUrl / firstCommentError say what became of the comment.

GET /api/posts returns the newest 50 rows by slot (?limit= up to 200), narrowed server-side by ?status= (any of proposed, scheduled, publishing, published, handoff, failed, canceled). ?sort=views instead ranks published posts over the whole history by their latest reading's impressions (views), most viewed first, narrowed by ?platform=, ?connectionId=, ?minViews= and ?since= (ISO). ?fresh=1 first re-reads, from the platforms, the numbers of the listed posts whose reading is older than 6 hours (up to 25 posts, so the answer can take ~30 s); without it every reading is as of its own fetchedAt. GET /api/posts/[id] returns the same shape for one row. Two fields close the loop between what an agent meant and what happened:

  • inspiration - what the post was made from: {kind, id, lane?, title?, url?} where kind is format-card, topical-story or reference-post. Set it on a delivery through the artifact's inspiredBy meta (the object, or a JSON string of it); anything malformed is dropped, never an error. null when nobody said.
  • metrics - how a published post did, null for anything not published and for a published post we hold no reading for yet: {latest, at24h, at7d, lift, baseline: {platform, n, median, scoredBy}}. Each sample is {fetchedAt, impressions, reactions, comments, shares}; at24h and at7d are the first readings taken at or after that age, null until one exists. Sampling slows as a post ages, so on a sparsely sampled post the "24h" reading can be days late - fetchedAt on the sample is what says how late, and it is on every sample for exactly that reason. Lift is this post's score divided by the median score of the same account's published posts on the same platform over the trailing 90 days
    • 2 means it did twice its channel's median. It is null when fewer than five scored posts stand behind that median (baseline.n says how many do), because a two-post median invents findings. baseline.scoredBy is impressions where the platform reports reach and engagements where it does not - the same rule the Performance screen ranks by.

/api/posts/[id]/boost takes {budgetCents, days, objective} (engagement or traffic) and spends nothing: it creates a proposed boost action, and only POST /api/actions/[id]/send on that action creates the Meta campaign. The budget is the campaign's lifetime cap, billed by Meta to the connected ad account. It refuses (402) without an active paid plan (the Free plan does not qualify), and (409) when the post is not a published Facebook Page or Instagram post with a known platform id, or when no active Meta ad account is connected.

GET /api/posts/[id]/comments answers {postId, platform, comments[], lastReadIso} from our own ledgers - it never calls a platform. Each comment carries {id, authorHandle, text, url, atIso, isOwn, replyVerdict, reply}, where reply is the drafted action bound to it ({actionId, text, status, verdict, error, url, sentAtIso}) or null. POST with {commentId, text} stages that reply as a proposed reply action - the same row the approvals inbox holds - which POST /api/actions/[id]/send then sends; it refuses (409) when the platform has no sanctioned reply path for that comment (replyVerdict: "task"). /comments/refresh runs the same engagement poll Cloud Tasks fires after publishing, and only spends credits when there is a new comment to draft a reply to.

Agents (strategies) and the manager

MethodPathScopePurpose
GET/api/strategiesreadThe agent roster.
POST/api/strategieswrite, or publish with autoPublishPostsCreate an agent.
PATCH/api/strategies/[id]write, or publish with autoPublishPostsEdit an agent.
POST/api/strategies/[id]/runpublishWake an agent now.
POST/api/strategies/suggestwriteOne engine-studied agent seed for the editor.
POST/api/manager/runpublish"Plan now" through the first active agent.
GET/api/manager/runs, /api/manager/runs/[id]readWake runs and their proposals.
POST/api/manager/runs/[id]/approvepublishApprove a run's proposals.
POST/api/manager/runs/[id]/dismisswriteDismiss a run with feedback.
GET/PUT/api/manager/settingsread / writeCadence and targeting.
GET/api/manager/budgetreadSpend, allowance, paused state.

Characters (actors)

An actor is workspace-level identity - a face, a voice, a style, a memory - that posts and agents CAST. characters on a job is that cast, by slug or id; the agent is shown the account's whole roster either way, so it can reuse an actor that fits instead of inventing another one. A ref that names nobody is a 400 naming it, never a run that silently stars nobody.

MethodPathScopePurpose
GET/api/charactersreadThe roster: identity, voice (kind + the direction a designed one was written from), locks.
POST/api/characterswriteCreate one: {name, bio?, isUser?}.
GET/api/characters/[id]readOne actor.
PATCH/api/characters/[id]writeEdit it, including the three locks.
DELETE/api/characters/[id]writeArchive it (posts and agents may still cast it).
POST/api/characters/[id]/voicewriteDesign, clone, or speak - see below.

POST /api/characters/[id]/voice takes one of:

  • {action: "design", voicePrompt} - writes a voice from casting direction. The direction is KEPT on the character and shown to the agent on every run.
  • {action: "clone", sampleMediaKey, consent: true} - clones a 5-10s sample already in the account. consent is the caller attesting they hold the rights; it is stored.
  • {action: "speak", text} - reads text in the actor's current voice, returns a mediaKey.

A creation SPENDS CREDIT: the answer carries costUsd (what the provider billed us) and the account is debited for it at the tier's rate, ledgered under voice:<design|clone>:<id>. An empty wallet is a 402 with the same trial_exhausted code a run gives - never a silent no-voice. speak is not a creation and is not billed here.

Engagement actions

MethodPathScopePurpose
POST/api/actions/[id]/sendpublishSend a drafted comment or reply, or run an approved boost or campaign change.
POST/api/actions/[id]/donewriteMark a task card done.
POST/api/actions/[id]/skipwriteSkip a proposed action.
MethodPathScopePurpose
GET/api/ads/campaigns/[id]?adAccountId=readOne Meta campaign as Meta has it now: status, budget, end date.
POST/api/ads/campaigns/[id]publishPause, resume, change the budget or the end date.
DELETE/api/ads/campaigns/[id]?adAccountId=publishDelete the campaign.

POST /api/ads/campaigns/[id] takes {adAccountId, status?, budgetCents?, endsAt?} with absolute targets (status is ACTIVE or PAUSED, budgetCents is in the budget's own kind, daily or lifetime, and endsAt is an ISO date in the future). A change that can only lower spend (pause, lower budget, earlier end) applies at once and answers {outcome: "applied", settings}. One that could raise it (resume, higher budget, a later end on a daily budget) spends nothing yet: it answers 201 {outcome: "proposed", action} with a proposed campaign_change action, and only POST /api/actions/[id]/send on that action applies it. Raising needs an active paid plan (402), and one waiting change per campaign (409). Only Meta campaigns are supported today.

Brand Memory, assets and media

MethodPathScopePurpose
GET/api/workspacereadThe brain's file tree with its completion ring.
GET/api/workspace/file?path=readOne file.
PUT/DELETE/api/workspace/filewriteWrite or delete one file.
GET/api/workspace/exportreadThe whole brain as a zip.
GET/api/workspace/onboardingreadWhat is still open, as the questions a human answers, with hints - see Brand Memory.
POST/api/brain/scanwriteRead a link and file what it says into Brand Memory (NDJSON stream).
POST/api/brain/importwriteImport a .md/.txt/.pdf/.docx document.
GET/api/brain/assets, /api/assetsreadBrand uploads, and the full asset library.
GET/api/brain/assets/[filename]readStream one asset (Range supported).
PUT/PATCH/DELETE/api/brain/assets/[filename]writeUpload, rename, delete an asset.
GET/api/media/[...key]readStream any media key this account owns.

Analytics

MethodPathScopePurpose
GET/api/metricsreadPublished posts with their latest samples and the 90-day series.
GET/api/metrics/history?post=readOne post's accrual curve against the account median.
GET/api/metrics/followersreadFollower counts and the weekly delta.
POST/api/metrics/refreshwriteRefresh stale samples (calls the platforms).

Profile audit

MethodPathScopePurpose
GET/api/profile/auditsreadLatest score and open fix count per connected account.
GET/api/profile/audits/[connectionId]readOne account's fix list, each with ready-to-paste copy, and its last two scores.
POST/api/profile/audits/[connectionId]writeRead the live profile and grade it again (spends credits; 429 when run too often).
PATCH/api/profile/audit-items/[id]write{status: todo | done | dismissed}.

Tools

Every tool the studio agent holds, callable on its own - the same code, so the API never does a different thing from the agent. GET /api/tools (no key) lists each one with its JSON Schema input and default price. The body of a call is that input, plus an optional budgetUsd (default 2, max 10); the credit is held for the budget and settled at what the call really cost.

MethodPathScopePurpose
GET/api/toolsnoneThe catalog: {tools: [{name, description, inputSchema, costUsd}]}.
POST/api/tools/[name]writeRun one tool. Answers {tool, output, media: [{key, url}], chargedUsd}.
POST/api/imageswritegenerate_image: {name, prompt, aspectRatio, size?, quality?, reference_keys?}.
POST/api/videos/generatewritegenerate_footage: AI footage from a prompt.
POST/api/videos/editwriteedit_footage: cut, reframe, cutout, captions, music, overlays on a video you uploaded.
POST/api/videos/demowritedemo_page: a recorded product demo of a page.
POST/api/carouselswriterender_carousel: PDF plus one PNG per slide.
POST/api/thumbnailswriterender_thumbnail: a frame of a video plus words.
POST/api/speechwritespeak: up to 2,000 characters read into a WAV. {text, voice?, character?, energy?, style?}.
POST/api/captionswritecaption_file: a word-timed .srt or .vtt for a video or audio file. {media_key, format?, names?}. Free.
curl -s https://shapelessai.com/api/images -H "Authorization: Bearer $SHAPELESS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"founder desk","prompt":"A quiet founder desk at dusk, one lamp, deep shadows.","aspectRatio":"4:5"}'

A file the tool made comes back in media with its URL; put its key on POST /api/posts mediaKeys to publish it. Refusals: 400 the input does not match the schema (the message names the field), 402 not enough credit (the body carries upgrade links for the human) or the call costs more than budgetUsd, 404 no such tool, 422 the tool refused the input, 502 the tool failed. A refusal or failure is never charged. Upload a video to edit with POST /api/studio/uploads first; its mediaKey is the input.

Speech. voice is one of six narrator voices - Sulafat (female, warm), Charon (male, informative; the default), Kore (female, firm), Achird (male, friendly), Erinome (female, clear), Schedar (male, even) - or pass character (an id or slug from GET /api/characters) to read in that character's designed or cloned voice. energy is calm, confident (default), energetic or hype; style adds direction in plain words.

curl -s https://shapelessai.com/api/speech -H "Authorization: Bearer $SHAPELESS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text":"We ship on Friday. Here is what changed.","voice":"Kore","energy":"energetic"}'

Captions. The same ear the video editor cuts by: every word timed, cut into cues of at most two 42-character lines and six seconds. names spells your brand your way. The file text comes back inline in output.text as well as filed at output.captionsKey. Burned-in captions are POST /api/videos/edit.

Planned routes

A planned capability's route exists and answers 501 {code: "not_live", want, docs, sketch} until it ships; in /api/openapi.json it is marked x-status: planned. The list, the sketches and the want door are on Capabilities.

These refuse a bearer key. It is not an oversight:

  • Key management (/api/keys, /api/keys/[id]) - a key must never be able to mint itself a wider key. A leaked read key stays a leak, not a foothold.
  • Money (/api/checkout, /api/portal) - buying and cancelling belong to the person paying.
  • Claiming a wiki key (/api/wiki/keys/claim) - the signed-in person says yes to a key an agent already holds, on the activation page.
  • Sign-in (/api/auth/*) and the platform OAuth flows (every /api/connections/<platform> start and callback) - browser redirect flows carrying platform grants. An agent gets the human there with POST /api/connections/link; the identity list, the link, groups, tracked accounts and disconnect all take a key.
  • Founder metrics (/api/metrics/agent-lanes*) and /metrics - gated on FOUNDER_EMAILS.
  • Webhooks and platform callbacks (/api/polar/webhook, /api/meta/*, /api/subscribe) - they carry their own signatures.
  • Internal cron (/api/internal/*) - x-internal-key, unchanged.
  • Dev (/api/dev/*) and the daily-brief routes (/api/brain/brief/*), which are browser surfaces today.

The other two faces

The CLI and the MCP server are this same contract, wrapped. There is one implementation of every operation underneath and one scope policy judging it, so anything you can do here you can do there, and the refusals are identical.

OpenAPI

124 paths, version 1. Point a generator at /api/openapi.json rather than reading this page.

  • /api/meGET
  • /api/me/emailGET · PUT
  • /api/wiki/identityGET
  • /api/platformsGET
  • /api/openapi.jsonGET
  • /api/ai-toolsGET
  • /api/ai-tools/requirementsGET
  • /api/ai-tools/statsGET
  • /api/ai-tools/previewPOST
  • /api/ai-tools/submitPOST
  • /api/ai-tools/{slug}GET · PATCH
  • /api/ai-tools/{slug}/verifyPOST
  • /api/ai-tools/{slug}/statsGET
  • /api/ai-tools/{slug}/clickPOST
  • /api/capabilitiesGET
  • /api/toolsGET
  • /api/tools/{name}POST
  • /api/imagesPOST
  • /api/videos/generatePOST
  • /api/videos/editPOST
  • /api/videos/demoPOST
  • /api/carouselsPOST
  • /api/speechPOST
  • /api/captionsPOST
  • /api/thumbnailsPOST
  • /api/capabilities/{id}/wantPOST
  • /api/connectionsGET · DELETE
  • /api/connections/linkPOST
  • /api/connections/{id}PATCH
  • /api/connections/{id}/tiktok-creator-infoGET
  • /api/connections/trackedPOST
  • /api/groupsGET · POST · PATCH
  • /api/groups/{id}PATCH · DELETE
  • /api/connections/{id}/queueGET · PATCH
  • /api/postsGET · POST
  • /api/v1/instagram/audioGET
  • /api/posts/capGET
  • /api/posts/{id}GET · PATCH · DELETE
  • /api/posts/{id}/approvePOST
  • /api/posts/{id}/publishPOST
  • /api/posts/{id}/mark-postedPOST
  • /api/posts/{id}/revisePOST
  • /api/posts/{id}/boostPOST
  • /api/posts/{id}/commentsGET · POST
  • /api/posts/{id}/comments/refreshPOST
  • /api/posts/resolvePOST
  • /api/inboxGET
  • /api/v1/jobsGET · POST
  • /api/v1/jobs/{id}GET
  • /api/v1/jobs/{id}/messagesPOST
  • /api/v1/jobs/{id}/runPOST
  • /api/v1/referencesGET · POST
  • /api/v1/references/{id}DELETE
  • /api/v1/references/suggestionsGET
  • /api/v1/references/suggestions/{id}/decidePOST
  • /api/v1/references/browseGET
  • /api/identity/searchGET
  • /api/identity/graphGET
  • /api/identity/homeGET
  • /api/identity/node/{id}GET
  • /api/identity/peopleGET
  • /api/identity/facetsGET
  • /api/identity/similarGET
  • /api/identity/libraryGET
  • /api/identity/trackedGET · POST · DELETE
  • /api/identity/pull-estimateGET
  • /api/identity/resolvePOST
  • /api/identity/expandPOST
  • /api/identity/postPOST
  • /api/studioPOST
  • /api/studio/tailGET
  • /api/studio/stopPOST
  • /api/studio/attachments/{filename}PUT
  • /api/studio/uploadsPOST
  • /api/studio/uploads/{sessionId}PUT
  • /api/studio/uploads/completePOST
  • /api/conversationsGET
  • /api/conversations/{id}GET · PATCH · DELETE
  • /api/conversations/{id}/openedPOST
  • /api/conversations/{id}/panelGET · POST
  • /api/conversations/current/panelGET
  • /api/strategiesGET · POST
  • /api/strategies/{id}PATCH
  • /api/strategies/{id}/runPOST
  • /api/strategies/suggestPOST
  • /api/manager/runPOST
  • /api/manager/runsGET
  • /api/manager/runs/{id}GET
  • /api/manager/runs/{id}/approvePOST
  • /api/manager/runs/{id}/dismissPOST
  • /api/manager/settingsGET · PUT
  • /api/manager/budgetGET
  • /api/charactersGET · POST
  • /api/characters/{id}GET · PATCH · DELETE
  • /api/characters/{id}/voicePOST
  • /api/actions/{id}/sendPOST
  • /api/actions/{id}/donePOST
  • /api/actions/{id}/skipPOST
  • /api/ad-accountsGET · DELETE
  • /api/ads/performanceGET
  • /api/ads/refreshPOST
  • /api/ads/campaigns/{id}GET · POST · DELETE
  • /api/workspaceGET
  • /api/workspace/fileGET · PUT · DELETE
  • /api/workspace/exportGET
  • /api/workspace/onboardingGET
  • /api/brain/scanPOST
  • /api/brain/importPOST
  • /api/brain/assetsGET
  • /api/brain/assets/{filename}GET · PUT · PATCH · DELETE
  • /api/assetsGET
  • /api/media/{key}GET
  • /api/metricsGET
  • /api/metrics/historyGET
  • /api/metrics/followersGET
  • /api/metrics/refreshPOST
  • /api/profile/auditsGET
  • /api/profile/audits/{connectionId}GET · POST
  • /api/profile/audit-items/{id}PATCH
  • /api/videos/renderPOST
  • /api/trendsGET
  • /api/influencersGET
  • /api/videos/edit-longPOST
  • /api/identitiesGET