Developer reference

FamScoop API

Build integrations, mobile shortcuts, and AI agents that write directly to the live site. Same data model as the web UI, no redeploys required.

Manage tokens at /settings/api-tokens. Manager / Admin role required.

FamScoop API v1

WordPress-style application passwords for FamScoop. Use them to write blog
posts, reviews, and community feed posts to the live site without redeploying
— from any AI agent (Claude, Make, n8n), CMS bridge, mobile shortcut, or
plain curl.

TL;DR

  1. Sign in to famscoop.com as a Manager / Admin.
  2. Go to More → API tokens (/settings/api-tokens).
  3. Click Create new token, pick scopes, copy the raw token once.
  4. Send it as Authorization: Bearer fams_… to any /api/v1/* endpoint.
curl -sS https://famscoop.com/api/v1/me \
  -H "Authorization: Bearer fams_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{
  "ok": true,
  "user": { "id": "...", "username": "yourname", "role": "ADMIN" },
  "auth": { "source": "token", "scopes": ["articles:write","me"], "tokenId": "..." }
}

Scopes

Scope What it allows
me Identify the caller (GET /api/v1/me).
articles:read List + read blog posts (incl. drafts).
articles:write Create + update blog posts.
reviews:read List + read reviews (incl. drafts).
reviews:write Create + update reviews, scene notes, episodes.
posts:read List + read community feed posts.
posts:write Create + update community feed posts.
categories:write Create blog categories.
lists:write Create + manage curated "best of" SEO lists.
taxonomy:read List categories, tags, groups, the content map.
uploads:write Upload images (returns CDN-style URL).

Pick the narrowest set for each integration. A "publish blog posts" bot only
needs articles:write + categories:write + taxonomy:read + uploads:write

  • me. A full SEO automation wants all of them.

Quick start for an AI agent

Give Claude a token, then have it call GET /api/v1/taxonomy first — that
single response returns every blog category, group, the enum vocabularies
(review types, post types, stoplight values), and the content-meter field
names. From there it knows exactly what category, type, groupId, etc. to
send when creating content. No guessing.


Endpoints

All endpoints accept JSON unless noted. Auth header is required everywhere.

GET /api/v1/me

Smoke-test. Returns { user, auth }.

GET /api/v1/articles

List blog posts.
Query: status (PUBLISHED/DRAFT/ALL, default PUBLISHED), limit
(1–100, default 20), cursor (id from previous nextCursor).

POST /api/v1/articles

Create a blog post.

{
  "title": "Is Roblox Safe for Kids in 2026?",
  "subtitle": "A 15-minute walkthrough of the actual settings that matter.",
  "category": "SCREEN_TIME",
  "content": "# heading\n\nMarkdown body…",
  "contentFormat": "markdown",
  "coverImage": "https://famscoop.com/uploads/covers/abc.webp",
  "tags": ["roblox", "safety"],
  "references": [
    { "title": "Roblox safety center", "url": "https://corp.roblox.com/safety", "publisher": "Roblox" }
  ],
  "metaTitle": "Is Roblox Safe? Parent Guide (2026)",
  "metaDescription": "…",
  "status": "PUBLISHED"
}

Response 201:

{ "ok": true, "article": { "id": "…", "slug": "is-roblox-safe-2026", "status": "PUBLISHED" }, "url": "/articles/is-roblox-safe-2026" }

GET /api/v1/articles/:id

Fetch a single article. :id accepts the cuid OR the slug.

PATCH /api/v1/articles/:id

Partial update. Send only the fields you want to change. To publish a draft:

{ "status": "PUBLISHED" }

GET /api/v1/reviews

List reviews. Same shape as /articles plus optional type filter
(MOVIE, TV_SHOW, GAME, AI_APP, BOOK, etc).

POST /api/v1/reviews

Create a review.

{
  "type": "MOVIE",
  "title": "Toy Story 5",
  "year": 2026,
  "mpaaRating": "G",
  "stoplight": "GREEN",
  "youngestAgeOk": 4,
  "ageRangeMin": 4,
  "ageRangeMax": 12,
  "releaseDate": "2026-06-19",
  "editorialSummary": "Pixar returns to Woody, Buzz, …",
  "editorialReview": "…",
  "parentsNeedToKnow": "Mild action, …",
  "bottomLine": "Looks safe for ages 4 and up.",
  "violenceScore": 1,
  "scaryScore": 1,
  "status": "PUBLISHED"
}

PATCH /api/v1/reviews/:id

Partial update by id OR slug.

POST /api/v1/posts

Create a community feed post (news share, recommendation, AI-summarized story).

{
  "type": "NEWS_SHARE",
  "title": "California passes new social-media age rule",
  "content": "California's new law requires…",
  "identityMode": "PUBLIC",
  "tags": ["news", "policy"]
}

Optional linkage:

  • reviewId — link the post to a specific review (shows on that review page).
  • articleId — link to a blog post.
  • groupId — post inside a specific community group.

POST /api/v1/uploads

Upload an image. Two shapes:

multipart/form-data (browser-style):

curl -sS https://famscoop.com/api/v1/uploads \
  -H "Authorization: Bearer fams_…" \
  -F "kind=covers" \
  -F "file=@./poster.jpg"

application/json with a remote URL — server fetches, compresses to <300 KB
WebP, and hosts it locally:

{ "url": "https://image.tmdb.org/t/p/original/poster.jpg", "kind": "covers" }

Response:

{ "ok": true, "url": "/uploads/covers/m1a2b3-x.webp", "size": 198321, "type": "image/webp" }

Concatenate with https://famscoop.com for absolute URLs.

GET /api/v1/taxonomy

The content map in one call — categories, groups, enum vocabularies, review
meter field names, and content counts. Call this first from any automation.

GET /api/v1/categories

List blog categories with published-article counts.

POST /api/v1/categories

Create a blog category (idempotent — returns the existing one if the slug
already exists).

{ "name": "Screen Time", "description": "Guides on healthy screen habits." }

Response includes category.id — pass it as categoryId when creating articles.

GET /api/v1/tags

Distinct tags in use across published articles + posts, with usage counts.
Reuse these instead of inventing near-duplicates.

GET /api/v1/groups

Public community groups (id, slug, name). Use a group id as groupId in
POST /api/v1/posts.

GET /api/v1/posts · GET|PATCH|DELETE /api/v1/posts/:id

List, read, update (pin/feature/lock/edit), and soft-remove feed posts.

POST /api/v1/reviews/:id/scene-notes

Bulk-add timestamped scene notes (the "what happens at X:XX" deep-SEO content).
:id is the review id or slug.

{
  "replace": false,
  "notes": [
    { "timestamp": "00:14:30", "category": "Scary", "severity": 3, "description": "A jump-scare in the basement." },
    { "timestamp": "00:48:10", "category": "Language", "severity": 2, "description": "One s-word during an argument." }
  ]
}

POST /api/v1/reviews/:id/episodes

Bulk-upsert episodes for a TV review (powers per-season + per-episode pages).
Idempotent per (season, episodeNumber).

{
  "episodes": [
    { "season": 1, "episodeNumber": 1, "title": "Chapter One", "stoplight": "YELLOW", "scaryScore": 3, "notes": "Sets the tone; one tense sequence." }
  ]
}

GET /api/v1/lists · POST /api/v1/lists

Curated "best of" SEO collections (the /best/[slug] landing pages). Create
with items inline — reference reviews/articles by id or slug:

{
  "title": "Best Family Movies for Ages 6–8 (2026)",
  "collectionType": "BY_AGE",
  "ageRange": "6-8",
  "intro": "Our hand-picked list…",
  "status": "PUBLISHED",
  "items": [
    { "reviewSlug": "the-wild-robot", "note": "A gentle masterpiece." },
    { "reviewSlug": "moana-2" }
  ]
}

GET|PATCH|DELETE /api/v1/lists/:id

Read, update (sending items REPLACES the whole ordered set), or archive a
list. :id is the list id or slug.

DELETE /api/v1/articles/:id · DELETE /api/v1/reviews/:id

Soft-archive (status → ARCHIVED). The URL keeps resolving but drops out of
listings + sitemap.


Using it from Claude (tool use)

Define a tool, then call your live site:

{
  "name": "famscoop_create_article",
  "description": "Publish a blog post to famscoop.com",
  "input_schema": {
    "type": "object",
    "required": ["title", "category", "content"],
    "properties": {
      "title": { "type": "string" },
      "category": { "type": "string", "enum": ["PARENTING", "SCREEN_TIME", "SAFETY", "EDUCATION"] },
      "content": { "type": "string", "description": "Markdown body" },
      "metaDescription": { "type": "string" },
      "status": { "type": "string", "enum": ["DRAFT", "PUBLISHED"] }
    }
  }
}

Tool handler (Node):

async function famscoop_create_article(input) {
  const res = await fetch('https://famscoop.com/api/v1/articles', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.FAMSCOOP_TOKEN}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(input),
  });
  return await res.json();
}

Now Claude (or any model) can write to your live site by calling that tool.


Error shape

Every error response is JSON:

{ "error": "Missing scope: articles:write" }
Status Meaning
401 No token, or token invalid / revoked / expired
403 Token valid but missing the required scope
404 Entity (article / review / post) not found
422 Validation failed — issues field has details
500 Server error

Security

  • Tokens are stored as SHA-256 hashes. The raw value cannot be recovered —
    if it leaks, revoke it; create a new one.
  • Tokens inherit the user's role; revoking the user revokes their tokens.
  • lastUsedAt is bumped on every request so you can spot stale clients.
  • Optional expiresAt (in days from creation) auto-disables a token.
  • Rate-limiting: API requests pass through the same protections as the web
    forms. Don't pound the endpoints.

Reference: response cache invalidation

After every successful write the server calls revalidatePath() on the
affected public routes. So POST /api/v1/articles flushes /blog, and
POST /api/v1/reviews flushes /discover. Pages re-render on the next
request — no manual cache flush needed.