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:
429withRetry-After;X-RateLimit-Limit/X-RateLimit-Remainingon every response. Idempotency-Keyheader on POST/PATCH/DELETE: the first response is stored for 24 hours and replayed (headerIdempotent-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
list_workspaces: pick a workspace slug.get_results: see which hooks, formats and times earn views.generate_posts: returns ajobId; pollget_generation_statusuntil done.get_candidateonce its state isready: review text and images.approve_candidateorreject_candidate;get_calendarto 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>/approveMCP
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/workspacesreadList 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}readGet 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* | path | string | Workspace slug, from list_workspaces. |
list_posts
GET /api/v1/postsreadList 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.
| workspace | query | string | Only this workspace. |
| status | query | draft | approved | scheduled | publishing | published | failed | needs_manual | |
| from | query | string | Inclusive start (ISO 8601). |
| to | query | string | Exclusive end (ISO 8601). |
| limit | query | integer | |
| offset | query | integer |
get_post
GET /api/v1/posts/{id}readGet 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* | path | string | Post id. |
list_candidates
GET /api/v1/candidatesreadList 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.
| workspace | query | string | Only this workspace. |
get_candidate
GET /api/v1/candidates/{id}readGet 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* | path | string | Candidate id. |
generate_posts
POST /api/v1/workspaces/{slug}/generateread_writeAsk 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* | path | string | Workspace slug, from list_workspaces. |
| count | body | integer | How many candidates. Default: the workspace's daily amount. |
| targetDate | body | string | Local date (YYYY-MM-DD) the candidates are meant for. Default: tomorrow in the workspace timezone. |
get_generation_status
GET /api/v1/jobs/{id}readStatus 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* | path | string | jobId returned by generate_posts. |
approve_candidate
POST /api/v1/candidates/{id}/approveread_writeApprove 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* | path | string | Candidate id. |
reject_candidate
POST /api/v1/candidates/{id}/rejectread_writeReject 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* | path | string | Candidate id. |
publish_now
POST /api/v1/posts/{id}/publish-nowread_writePublish 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* | path | string | Post id. |
retry_post
POST /api/v1/posts/{id}/retryread_writeRetry 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* | path | string | Post id. |
| platform | body | instagram | facebook_page | tiktok | bluesky | threads | Retry only the target(s) on this platform. Required to retry a needs_check target (confirms you checked the account). |
| handle | body | string | With `platform`: the account handle, when the post has several targets on that platform. |
update_post
PATCH /api/v1/posts/{id}read_writeEdit 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* | path | string | Post id. |
| caption | body | string | |
| hashtags | body | array | |
| scheduledAt | body | string | New publish time. Must be in the future. |
reschedule_post
MCP tool onlyread_writeConvenience 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* | argument | string | Post id. |
| scheduledAt* | argument | string | New publish time, in the future. |
delete_post
DELETE /api/v1/posts/{id}read_writePermanently 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* | path | string | Post id. |
create_post
POST /api/v1/postsread_writeBring 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* | body | string | Workspace slug, from list_workspaces. |
| images* | body | array | Images in slide order. |
| caption* | body | string | |
| tiktokCaption | body | string | Defaults to caption. |
| hashtags | body | array | |
| platforms | body | array | Default: 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/resultsreadHow 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.
| workspace | query | string | Only this workspace; default all of them. |
| recent | query | integer | How many recent scored posts to include. |
get_calendar
GET /api/v1/calendarreadPosts 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.
| workspace | query | string | Workspace slug, from list_workspaces. |
| from | query | string | Inclusive start. Default now. |
| to | query | string | Exclusive 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.