webhouse.appwebhouse.appdocs

How AI sessions and external services create and publish CMS content programmatically — endpoint, JSON shape, access tokens, and what happens after a successful POST.

When to use this

This guide is for AI sessions, scheduled jobs, and external services that need to push content into a CMS site without going through the admin UI. Typical cases:

  • A research-agent session writes a markdown article and publishes it to your blog
  • A daily cron drops a digest post compiled from external sources
  • A webhook from a third-party tool (Zapier, n8n) creates documents on demand

If you are a human editing through webhouse.app/admin, you don't need this — just use the editor.

The contract in one paragraph

POST your document JSON to /api/cms/{collection}?site={siteId} with an Authorization: Bearer wh_... header carrying a token scoped content:write for that site. The body needs only four fields (slug, status, data, optionally locale); the server fills in id, _fieldMeta, updatedAt, and audit metadata. Use status: "published" — drafts do not trigger the live-site revalidation webhook.

Step 1 — Create an access token

In webhouse.app/admin:

  1. Click your avatar → Account PreferencesAccess tokensNew token
  2. Set scope to content:write and resource to the specific site (site:trail, not site:*)
  3. Optionally restrict to a single collection (posts only) for least-privilege
  4. Copy the generated wh_... string — it is shown once only

For cross-session use (e.g. handing the token to a different cc session), the user owning the token must paste it into the recipient's environment. There is no secure cross-session sharing primitive yet.

Step 2 — POST the document

bash
curl -X POST 'https://webhouse.app/api/cms/posts?site=trail' \
  -H 'Authorization: Bearer wh_xxx' \
  -H 'Content-Type: application/json' \
  -d '{
    "slug": "three-architectures-of-agent-memory",
    "status": "published",
    "data": {
      "title": "Three architectures of agent memory — and why Trail picked Compile",
      "excerpt": "RAG, fine-tuning, and compile-time integration each solve a different memory problem. Here is what each gives up — and why Trail spent a year building the third.",
      "content": "## Section heading\n\nMarkdown body…\n\n{{svg:scan-wall-curve}}\n\nMore prose…",
      "date": "2026-05-03",
      "author": "Trail Team",
      "category": "research",
      "tags": ["rag", "llm-wiki", "agent-memory"],
      "readTime": "9 min read"
    }
  }'

Response on success (HTTP 201):

json
{
  "id": "three-architectures-of-agent-memory",
  "slug": "three-architectures-of-agent-memory",
  "status": "published",
  "locale": "en",
  "data": { /* echoed back */ },
  "updatedAt": "2026-05-03T15:42:11.000Z",
  "_fieldMeta": {}
}

What the body fields mean

FieldRequiredNotes
slugyesURL-safe (a-z0-9-), unique within the collection. Becomes the URL fragment after urlPrefix.
statusyes"published" or "draft". Use "published" unless you specifically want to stage drafts — draft documents do not fire the revalidation webhook, so the live site stays stale.
datayesObject matching the collection's field schema. Field names must match cms.config.ts exactly. Unknown fields are stripped silently.
localenoDefaults to config.defaultLocale. Set explicitly if the site is multilingual and you are creating a non-default-locale variant.
translationGroupnoUUID linking translations of the same document. Required for multilingual sites — use the SAME UUID for all language variants. Generate with crypto.randomUUID().

What happens after the POST

  1. The document JSON is written to _data/sites/{siteId}/content/{collection}/{slug}.json on the cms-admin server (or to the GitHub repo for github-adapter sites)
  2. cms-admin saves a revision under _revisions/
  3. If the site has revalidateUrl configured, the revalidation webhook fires — Next.js sites call revalidatePath() for the affected route within ~2 seconds
  4. If the site uses GitHub Pages and deployOnSave: true, the rocket pipeline runs: build.ts produces the static output, the diffed files upload to the gh-pages branch, GitHub Pages publishes, and the page_build webhook fires back to cms-admin
  5. The admin UI shows a toast and (if Web Push is enabled) a native OS notification

For a clean GH-Pages site like trail-landing, end-to-end POST → live URL is typically 30–90 seconds.

Multilingual posts

For a site with da and en locales, create one document per language with a SHARED translationGroup:

bash
UUID=$(node -e 'console.log(crypto.randomUUID())')

# English variant
curl -X POST '.../api/cms/posts?site=trail' -d "{
  \"slug\": \"my-post\",
  \"locale\": \"en\",
  \"translationGroup\": \"$UUID\",
  \"data\": { /* English content */ }
}"

# Danish variant — SAME UUID
curl -X POST '.../api/cms/posts?site=trail' -d "{
  \"slug\": \"mit-indlaeg\",
  \"locale\": \"da\",
  \"translationGroup\": \"$UUID\",
  \"data\": { /* Danish content */ }
}"

Without shared translationGroup, the language switcher, hreflang tags, and side-by-side translation editor all break.

Updating an existing document

Use PATCH for partial update or PUT for full replace:

bash
curl -X PATCH 'https://webhouse.app/api/cms/posts/{slug}?site=trail' \
  -H 'Authorization: Bearer wh_xxx' \
  -H 'Content-Type: application/json' \
  -d '{"data": {"title": "Updated title"}}'

PATCH merges the supplied data keys with the existing document. Other top-level fields (status, locale) update independently.

SVG and other build-time assets

The Content API does NOT handle build-time assets like {{svg:...}} placeholders that a custom build.ts resolves at compile time. Those live in the site's source repository, not the CMS:

  • For a GitHub-backed site: open a PR adding apps/landing/public/uploads/svg/<slug>.svg plus any build.ts changes that wire up new slugs
  • For a CMS-uploaded image (used in a richtext editor or image field): use POST /api/media with multipart form-data

The distinction: if the asset is referenced in your build.ts at compile time, it belongs in the site source. If it is rendered into editor content, it belongs in CMS media.

Common mistakes

  • POST without ?site=... — token-based callers MUST always include ?site=<siteId> on the URL. The server falls back to the registry default site when omitted, which silently writes to the WRONG site. The token's site-scope claim is checked against ?site=, not against fallbacks.
  • Status "draft" — silently does not appear on the live site because revalidation does not fire. Use "published".
  • Field names with typos — unknown fields are silently dropped. The document is created but missing data. Run GET /api/cms/{collection}/{slug} to verify the round-trip.
  • Multilingual without translationGroup — orphans the document from its translation group. Side-by-side editor and language switcher break.
  • Reusing an existing slug — POST returns 409. Use PATCH instead, or pick a unique slug.

Verifying success

bash
# Did it land?
curl 'https://webhouse.app/api/cms/posts/{slug}?site=trail' \
  -H 'Authorization: Bearer wh_xxx'

# Is it live on the deployed site?
curl -I 'https://www.your-site.com/posts/{slug}/'

The second should return HTTP/2 200. If it returns 404, the build/deploy pipeline did not fire — check _data/sites/{siteId}/deploy-log.json for the most recent entry.

See also

Tags:apicontentautomationai-sessions
Previous
MCP Server — Authenticated Content Production
Next
Headless Site API & Chat Embedding
JSON API →Edit on GitHub →