follow.fast API

Drive follow.fast from your own tools or an AI agent: generate slideshow candidates, review and approve them, publish, and read results. REST under /api/v1, an MCP server at /api/mcp, an OpenAPI 3.1 spec and an agent skill (SKILL.md).

Authentication

Create a key under Account → API keys (shown once; read only or read and write) and send it as a bearer token. Keys belong to one organization; ids from other organizations return 404. Sessions and cookies are never accepted on these routes.

curl -H "Authorization: Bearer ff_..." https://app.follow.fast/api/v1/workspaces

Conventions

  • Success: { "data": ..., "meta": { total, limit, offset } }. Error: { "error": { "code", "message", "details?" } } with 400/401/403/404/405/409/413/422/429.
  • 403 insufficient_scope: a read-only key called a mutation. 422 validation_failed: bad input (unknown fields are rejected, so typos are caught).
  • Rate limit: 60 requests/minute sustained with a burst of 120, per key (token bucket held in the web process memory). Over the limit: 429 with Retry-After; X-RateLimit-Limit/X-RateLimit-Remaining on every response.
  • Idempotency-Key header on POST/PATCH/DELETE: the first response is stored for 24 hours and replayed (header Idempotent-Replayed: true) for the same key and request; the same key with a different request is 422.
  • Times are ISO 8601; posts are scored on views frozen at 72 hours.

The daily loop

  1. list_workspaces: pick a workspace slug.
  2. get_results: see which hooks, formats and times earn views.
  3. generate_posts: returns a jobId; poll get_generation_status until done.
  4. get_candidate once its state is ready: review text and images.
  5. approve_candidate or reject_candidate; get_calendar to confirm.
curl -s -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"count":3}' https://app.follow.fast/api/v1/workspaces/my-app/generate
curl -s -H "Authorization: Bearer $KEY" https://app.follow.fast/api/v1/jobs/<jobId>
curl -s -X POST -H "Authorization: Bearer $KEY" https://app.follow.fast/api/v1/candidates/<id>/approve

MCP

Streamable HTTP, JSON-RPC 2.0, stateless, at POST https://app.follow.fast/api/mcp with the same bearer key. Read-only keys only list the read tools.

# Claude Code
claude mcp add --transport http follow-fast https://app.follow.fast/api/mcp \
  --header "Authorization: Bearer $FOLLOWFAST_API_KEY"
// Cursor: ~/.cursor/mcp.json
{ "mcpServers": { "follow-fast": { "url": "https://app.follow.fast/api/mcp", "headers": { "Authorization": "Bearer ff_..." } } } }
// Claude Desktop: claude_desktop_config.json
{ "mcpServers": { "follow-fast": { "command": "npx",
  "args": ["-y", "mcp-remote", "https://app.follow.fast/api/mcp", "--header", "Authorization:${FF_AUTH}"],
  "env": { "FF_AUTH": "Bearer ff_..." } } } }

Operations

list_workspaces

GET /api/v1/workspacesread

List the workspaces (one per product/app) of your organization with their brand, posting plan and connected accounts. Call this first: the `slug` identifies a workspace in every other operation.

get_workspace

GET /api/v1/workspaces/{slug}read

Get a workspace by slug: brand voice, posting plan (posts per day, windows, timezone), autopilot flag and connected accounts. Returns 404 if the slug is not in your organization.

slug*pathstringWorkspace slug, from list_workspaces.

list_posts

GET /api/v1/postsread

List posts (one per platform group: a Meta post for Instagram+Facebook, a TikTok post), newest first. Filter by workspace slug, status (draft, approved, scheduled, publishing, published, failed, needs_manual) and a time window; `from`/`to` apply to the scheduled time, or the creation time for unscheduled posts. Each item has `displayStatus` (status refined: a failed post with some targets published is "partial"), targets (per-platform status, permalink, error) and a preview image URL. Use get_post for full metrics. Rejected candidates are not listed.

workspacequerystringOnly this workspace.
statusquerydraft | approved | scheduled | publishing | published | failed | needs_manual
fromquerystringInclusive start (ISO 8601).
toquerystringExclusive end (ISO 8601).
limitqueryinteger
offsetqueryinteger

get_post

GET /api/v1/posts/{id}read

Get one post with all image URLs (in slide order), caption, hashtags, schedule, per-platform targets (status, permalink, error) and metrics (views, likes, saves, and the frozen 72h snapshot with score/verdict once available).

id*pathstringPost id.

list_candidates

GET /api/v1/candidatesread

List generated candidates that are waiting for review (status draft, not rejected). A candidate is one slideshow idea with a feed variant (Instagram/Facebook) and a vertical variant (TikTok). `state` is pending, rendering, ready or error: only `ready` candidates can be approved. This is where new candidate ids show up after generate_posts.

workspacequerystringOnly this workspace.

get_candidate

GET /api/v1/candidates/{id}read

Get a candidate by id: its state, hook, slide texts, captions per platform, and the posts (with image URLs) it produced. Works for pending, approved and rejected candidates too.

id*pathstringCandidate id.

generate_posts

POST /api/v1/workspaces/{slug}/generateread_write

Ask follow.fast to generate new slideshow candidates for a workspace, using its brand, hooks, personas and past results. Asynchronous: returns 202 with a `jobId` immediately; poll get_generation_status until it is `done` (usually under a few minutes) and it lists the new candidates, or call list_candidates. New candidates are drafts and must be approved (approve_candidate) before anything is published. Max 10 per call.

slug*pathstringWorkspace slug, from list_workspaces.
countbodyintegerHow many candidates. Default: the workspace's daily amount.
targetDatebodystringLocal date (YYYY-MM-DD) the candidates are meant for. Default: tomorrow in the workspace timezone.

get_generation_status

GET /api/v1/jobs/{id}read

Status of a generate_posts job: queued, running, done or failed. When done, `candidates` lists the candidates it created (they may still be rendering: check each candidate's `state`).

id*pathstringjobId returned by generate_posts.

approve_candidate

POST /api/v1/candidates/{id}/approveread_write

Approve a ready candidate: both its posts get the next free slots from the workspace's posting plan and are published automatically at those times (use update_post/reschedule_post to move one, publish_now to go immediately). Fails with 409 if the candidate is still rendering or has nothing left to approve, and 422 if TikTok Direct Post settings are incomplete. Review the candidate first (get_candidate): this is the step that commits to posting.

id*pathstringCandidate id.

reject_candidate

POST /api/v1/candidates/{id}/rejectread_write

Reject a draft candidate so it is never posted. Only works while its posts are still drafts (409 otherwise). Rejected candidates disappear from list_candidates and list_posts.

id*pathstringCandidate id.

publish_now

POST /api/v1/posts/{id}/publish-nowread_write

Publish a draft, approved or scheduled post immediately instead of waiting for its slot. Returns the post; publishing is asynchronous, so poll get_post for target status and permalinks. 409 if already publishing/published or still rendering; 422 if TikTok settings are incomplete.

id*pathstringPost id.

retry_post

POST /api/v1/posts/{id}/retryread_write

Retry a post whose status is failed or needs_manual (read each target's `status` and `error` with get_post first; some failures need a reconnected account). Only targets that are not already published are republished, so a partly published post (displayStatus "partial") never double-posts. Targets with status needs_check (the publish call had an unknown outcome: it may be live) are NOT retried by a plain retry: check the account, then pass `platform` (and `handle` when the workspace has several accounts on it) to retry just that target. With `platform` only the matching failed or needs_check target(s) are retried.

id*pathstringPost id.
platformbodyinstagram | facebook_page | tiktok | bluesky | threadsRetry only the target(s) on this platform. Required to retry a needs_check target (confirms you checked the account).
handlebodystringWith `platform`: the account handle, when the post has several targets on that platform.

update_post

PATCH /api/v1/posts/{id}read_write

Edit an unpublished post (draft, approved, scheduled or failed): `caption`, `hashtags` (array, # optional) and/or `scheduledAt` (ISO 8601, in the future; only for approved or scheduled posts, drafts get their slot at approval; null is not allowed). Moving a scheduled post cancels its queued publish job and re-queues it for the new time. Banned words for the workspace are rejected (422). Slide text and images are edited in the web app, not here.

id*pathstringPost id.
captionbodystring
hashtagsbodyarray
scheduledAtbodystringNew publish time. Must be in the future.

reschedule_post

MCP tool onlyread_write

Convenience for update_post with only `scheduledAt`: move an approved or scheduled post to a new future time (ISO 8601). Drafts have no time until approved.

id*argumentstringPost id.
scheduledAt*argumentstringNew publish time, in the future.

delete_post

DELETE /api/v1/posts/{id}read_write

Permanently delete a post that has not been published (not publishing, published, or with any published target). Use reject_candidate to discard an unreviewed candidate instead. Cancels a queued publish job.

id*pathstringPost id.

create_post

POST /api/v1/postsread_write

Bring your own content: create draft posts from images you already have (public URLs or base64; REST also accepts multipart/form-data with `images` files). One carousel (or single image) for Instagram/Facebook (and Threads / Bluesky when connected and requested or enabled) and one TikTok photo post are created from the same images, as a candidate that you then approve_candidate or publish_now. Nothing is published by this call. 2-10 images make a carousel. Captions are checked against platform limits and the workspace's banned words.

workspace*bodystringWorkspace slug, from list_workspaces.
images*bodyarrayImages in slide order.
caption*bodystring
tiktokCaptionbodystringDefaults to caption.
hashtagsbodyarray
platformsbodyarrayDefault: every connected platform of the workspace; Threads and Bluesky join by default only when the workspace's posting plan has them switched on. They take the feed images (Bluesky: the first 4).

get_results

GET /api/v1/resultsread

How posts performed. Posts are scored on views frozen at 72h relative to the median of the same workspace and platform; a verdict (scale / keep / retire) needs at least 3 scored posts per option. Returns per-dimension tables (hook, format, template, persona, slot, slides, type: n, meanScore, medianViews, verdict), a weekly trend and the most recent scored posts. Use it to decide what to make more of before generating.

workspacequerystringOnly this workspace; default all of them.
recentqueryintegerHow many recent scored posts to include.

get_calendar

GET /api/v1/calendarread

Posts with a scheduled/publish time in a window, oldest first, with status and preview image. Default window: now to 14 days ahead (max 92 days). Use it to find free days before approving more, or to see what is going out.

workspacequerystringWorkspace slug, from list_workspaces.
fromquerystringInclusive start. Default now.
toquerystringExclusive end. Default from + 14 days.

get_asset GET /api/v1/assets/{id}

The bytes behind the image URLs in responses. Same bearer key; 404 for other organizations' assets.