FOUNDATION

Content API

1. Get a key

An administrator makes one in the site's admin: Settings → CMS → API keys → New key. Choose what it may do:

TemplateReach
Read onlyThe schema, the generated contract, every type's published content, and taxonomy terms.
Content posterRead and write the types you pick, and upload media — for an automation that publishes one kind of thing.
Read & writeEverything: all content, terms and their page copy, and media.

The key is shown once. Keep it in a secret store, never in a page, a repository or a log. Edit, rotate, revoke or delete it from the same screen. Copy instructions for AI on that screen copies a ready briefing for an AI agent, with this site's address in it.

2. Address and authentication

Base URL   https://hq.<your domain>/api/v1
Header     Authorization: Bearer <API key>

GET /api/v1/ping answers without a key, for uptime checks.

3. Start here

CallWhy
GET /api/v1Proves the key: the site, what this key may do, and the endpoints.
GET /api/v1/helpEvery endpoint on this site — one row per content type — marked with whether your key may call it. The source of truth; this page is the guide.
GET /api/v1/schemaThe content model: each type, and each field's id, type and rules. Values are written by field id.
GET /api/v1/contractThe generated TypeScript contract for this site's model, as text.

4. Read content

GET /api/v1/content/{type}?limit=50&offset=0      published entries, whole records
GET /api/v1/content/{type}/{slug}                  one entry

Each entry comes whole, with its public path and its parent. limit is 1–100; a full page carries next, the offset of the following one. ?status=draft lists drafts instead (oldest first) and needs the content:write:{type} capability for that type — a Content poster or Read & write key, not a Read only one.

5. Write content

POST /api/v1/content/{type}
{
  "slug": "spring-open-day",            // optional
  "status": "draft",                    // draft (default) · published · scheduled
  "publishedAt": "2026-11-01",          // a past date backdates; a future one with "scheduled" publishes then
  "parent": null,                       // nest under another entry of a hierarchical type (slug or id)
  "values": { "fld_title": "Spring open day", "fld_body": "<p>…</p>" }
}

PUT /api/v1/content/{type}/{slug}       // merges the values you send; records a revision first
  • New entries are drafts unless you send "status": "published".
  • Entry pickers take entry ids or slugs. Category pickers take term ids, slugs or labels; an unknown label creates the term, listed in termsCreated.
  • Image fields take a key from the media upload below.
  • Moving an entry (a new parent) adds 301s from its old address, and from everything beneath it.
  • The platform's own fields cannot be written by key; the reply names them.

6. Upload media

POST /api/v1/media?filename=front-door.jpg&alt=The%20front%20door&description=Opening%20day%2C%20March%202026&path=gallery
Content-Type: image/jpeg
<the file's bytes>

→ { "id": "…", "key": "ab/cd/abcd…", "mime": "image/jpeg", "bytes": 182345, "width": 2400, "height": 1600 }

Use the returned key as an image field's value. alt is what the image shows, for someone who cannot see it; description is the library's own note on the file (who or what, where and when — up to 2,000 characters); path files it into a folder. All three are optional.

7. Taxonomy terms

GET /api/v1/terms/{taxonomy}
GET /api/v1/terms/{taxonomy}/{slug}
PUT /api/v1/terms/{taxonomy}/{slug}    { "label", "description", "body", "seoTitle", "seoDescription", "image" }

A term's page copy: fields merge over what is stored, null or "" clears one. seoTitle ≤ 120 and seoDescription ≤ 320 characters; body is HTML.

8. Errors

Every refusal is JSON with an error sentence.

StatusMeans
400The request could not be read (malformed JSON, a missing part).
401The key is missing, wrong or revoked — one sentence for all three, deliberately.
403The key may not do that; the reply's need names the capability. Also: writing the platform's own fields.
404No such type, entry or term on this site.
405That method is not taken at that address.
422A value broke a field's rules, or a field id is not on that type; the reply names them.

9. A whole example

KEY="…"; SITE="https://hq.example.co.uk/api/v1"

curl -s "$SITE" -H "Authorization: Bearer $KEY"                 # what may this key do?
curl -s "$SITE/schema" -H "Authorization: Bearer $KEY"          # which field ids does "post" have?

KEY_IMG=$(curl -s -X POST "$SITE/media?filename=cover.jpg&alt=Our%20new%20studio" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: image/jpeg" --data-binary @cover.jpg | jq -r .key)

curl -s -X POST "$SITE/content/post" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d "{\"values\": {\"fld_title\": \"We moved\", \"fld_cover\": \"$KEY_IMG\"}}"   # saved as a draft