Use Forumulate as your website's CMS
Your marketing site is your own code — Next.js, Astro, Nuxt, SvelteKit, anything that can make an HTTP request. Forumulate holds its content: the pages, posts, menus, settings and pictures your marketing team edits without a developer, and that your site reads at build time or on each request.
This page covers the concepts, the keys, and the delivery API. Build a Next.js site on a content library takes you from an empty folder to a deployed site whose pages, menus and settings all come from Forumulate, with edits live within seconds.
The concepts
Content libraries
A content library belongs to your organization, like Media and Forms. An organization can have several — usually one per website. A library holds:
its model: the collections and globals, their fields, and the block types pages are built from;
the entries themselves, with drafts, review, scheduling, history and translations;
its keys (delivery and preview), webhooks and preview URLs;
who may edit it: content capabilities at the organization reach every library, and a per-library grant gives somebody one website's library only.
Find it under Content in the console. A library you cannot read does not exist as far as you are concerned: every route answers 404 for it.
Start one from the Website template and it comes with a whole site's model, ready to fill in:
POST /orgs/{orgId}/content/libraries
{ "name": "Marketing site", "template": "website" }The answer's library.id is the id every other call uses. The template creates no entries — the words on your website are yours.
Collections and globals
The console calls them Collections and Globals; the API calls both content types (GET …/types lists them, each with singleton: true for a global).
A collection has many entries: pages, posts, team members, case studies, menus. Each entry has a title, a slug and its fields.
A global has exactly one entry (per locale): the site settings, the header, the footer, the interface strings. You read it by its key.
The Website template declares:
Content type | Kind | What it is |
|---|---|---|
| Collection | A page built from blocks: |
| Collection | A dated article: |
| Collection | A path on your site ( |
| Collection | A navigation menu ( |
| Global | Name, logo, favicon, social links, default SEO, analytics ids, the announcement bar. |
| Global | The menu at the top and the button beside it. |
| Global | The footer menu, small print and copyright. |
| Global | Short interface copy (button labels, messages), translated per locale. |
These are ordinary content types. Rename a field, add a block type or a whole new collection, and the change sits on top of the template rather than replacing it.
Routes: the site map is content too
A route says which entry lives at which path. A marketer adds a page by creating the page, creating a route for it and publishing both — your site looks the path up and renders whatever is there, with no code change and no deploy. Paths are unique, normalised (/pricing, never pricing/), and a route can be disabled without losing its history.
Links inside content (menu items, buttons) point at the entry, not at its path, so moving a page to a new path keeps every link to it working. When a routed entry's path changes, a redirect from the old path is recorded automatically.
Keys
Your site reads a library with a key. Keys are made per library, in Content → Keys, or with the API (content.keys capability; a token creating one needs the admin scope):
POST /orgs/{orgId}/content/libraries/{libraryId}/keys
{ "name": "marketing site (server)", "kind": "delivery" }{
"key": { "id": "…", "kind": "delivery", "prefix": "cfd_", "origins": null, "contentTypes": null, "locales": null },
"secret": "cfd_…" // shown once, stored hashed
}Kind | Prefix | Reads | Where it may live |
|---|---|---|---|
|
| Published content | Your server only. Never in a browser bundle. |
|
| Published content, only from the | Page source is fine — that is what it is for. |
|
| Drafts and scheduled content, and | Your server only. |
Every key works only on the delivery API. A delivery key sent to a management route is refused, so a key that leaks can read your published content and nothing else.
Narrow a key with
contentTypesandlocales; omitted means all of them. A publishable key requiresorigins.expiresInDaysmakes a key expire on its own.Rotate with
POST …/keys/{keyId}/rotate: you get a new secret, and the old one keeps working forgraceMinutes(default 60) so you can redeploy without downtime.0retires it at once — for a key that leaked.Revoke with
DELETE …/keys/{keyId}. The next request with it is a 401.cfd_andcfv_are registered with GitHub secret scanning. Commit one to a public repository and it is revoked: a preview key at once, a delivery key after 24 hours, so you can rotate it without your site going dark.cfp_is not registered, because it is public by design.
Preview sessions
A preview key reads every draft for as long as it lives. To show an editor a draft on your site you do not need one: a preview session does it with nothing standing.
The editor presses Preview on the entry's card in the studio (Content → the entry → Status). The studio mints a token with the editor's own session (
content.readon the library), bound to that library, that entry and the path the entry renders at — the path of a route pointing at it, else its collection'spreviewUrl. The token is single-use and valid for ten minutes. An entry with no page on the site has the button disabled, saying so.The studio POSTs the token as a form field, with the
path, to your site's preview route on the library's first preview origin — never in a URL. The route is/api/previewunless you set another under Settings → Delivery → Preview route (settings.preview.route).Your server exchanges it at
POST /delivery/v1/preview/exchangewith{ "token", "path", "library" }and gets{ "credential": "cfvs_…", "expiresAt", "libraryId" }: a delivery bearer that reads that library's drafts for one hour. Keep it in an httpOnly cookie, never in page script.
Scripts and other tools can mint a token the same way the studio does: POST /orgs/{orgId}/content/libraries/{libraryId}/preview/sessions with { "entryId": "…", "path": "/pricing" } answers { "session": { "token": "cfvt_…", "expiresAt": "…", "libraryId", "entryId", "path" } }.
A second use of the token, a different path, another library or an expired token is a 401 with the same message whichever it was. The credential reads as the editor who minted it, re-checked on every request, so removing them from the library ends their previews at once. It works on the /libraries/{libraryId}/… delivery routes only, never the shared-cache /k/ form. Preview sessions need the content.preview plan feature.
The framework helpers implement the site's half — createPreviewRoute and createExitPreviewRoute for Next (draft mode), Astro, Nuxt and SvelteKit: POST only, relative paths only, and an HttpOnly; Secure; SameSite=None; Partitioned cookie so the preview still works inside the studio's frame. See Preview in the Next.js guide.
Developing against localhost. The console only frames your site's preview origins. To preview a site running on your machine, turn on Allow localhost in the library's preview settings (settings.preview.allowLoopback); it lets that library's studio frame http://localhost:* and http://127.0.0.1:*, and nothing else. Turn it off when the site goes live.
The key list shows each key's last use and request counts (updated about once a minute). Secrets are never listed.
The delivery API
Every route lives under:
https://forumulate.com/api/delivery/v1/libraries/{libraryId}/…with the key in the Authorization header:
curl -H "Authorization: Bearer $FORUMULATE_DELIVERY_KEY" \
-A 'my-site/1.0' \
"https://forumulate.com/api/delivery/v1/libraries/$LIBRARY/routes/resolve?path=/pricing"Use the library id rather than its slug: cache tags and webhooks carry ids, so a site that reads by slug cannot be revalidated by tag. Set a non-default User-Agent, as for every API call.
Route | What it answers |
|---|---|
| The model as your key may read it: each type's key, label, |
| A list of a collection's entries. |
| The same, with the query in the body (for a |
| One entry. |
| A global ( |
| Every live path and the entry it shows — for prerendering and sitemaps. |
| What lives at a path: the entry, a redirect to follow, or 404. |
| The redirect table, for |
| Routed paths with their last change. |
| Full-text search over the fields a type marks searchable. |
| JSON Schema and an OpenAPI document of your model, typed per content type. |
One entry
{
"id": "6f1c…",
"type": "post",
"slug": "launch",
"title": "Introducing Brightfield",
"locale": "en",
"status": "published",
"publishedAt": "2026-09-02T09:00:00.000Z",
"updatedAt": "2026-09-02T09:00:00.000Z",
"fields": { "author": "Maya Okafor", "excerpt": "…", "body": { "type": "doc", "content": [] } },
"route": "/blog/launch", // its path, when a route points at it
"locales": ["en", "es"],
"_links": { "self": "/delivery/v1/libraries/…/types/post/entries/6f1c…" }
}Fields marked internal never appear; fields marked preview-only appear for preview keys only.
Lists, filters and paging
GET …/types/post/entries?where[author][equals]=Maya%20Okafor&sort=-publishedAt&limit=10where:{field: {operator: value}}, withand/orarrays, as query brackets or a JSON string. Operators:equals,not_equals,in,not_in,contains,like,exists,gt,gte,lt,lte,near. Dot paths reach into groups (hero.heading),[]into rows, andauthor.nameinto references (withdepth).sort:-publishedAt,title.select:title,hero.heading— only these fields.localeandfallback=truefor untranslated fields.Cursor paging only.
limit(up to 100) andcursor. Every list answers{ data, page: { limit, hasMore, nextCursor } }: walknextCursorwhilehasMore. Addcount=trueforpage.totalwhere it is cheap to count. There is no page number or offset.
A filter on a field that is not indexed, over a large collection, is a 422 naming the field — mark it indexed in the model, rather than have the query slow down as the collection grows. Every malformed parameter is a 422 that names the parameter.
References
depth (0–3, capped by your plan) populates references in place: a menu's links, a post's author, a page's button target. Each populated entry has the same shape as above, including its route. Past depth, a reference is a stub: { kind, id, contentType, slug, title }. A reference to something that cannot be served — unpublished, deleted, a type your key cannot read — is null. Your site never receives an id it cannot fetch. populate[author]=name,avatar projects what a populated type brings.
Pictures
A media field is served as { url, width, height, alt, caption, focal, variants }. variants maps widths to pre-sized URLs, so a srcset needs no image service of yours. @forumulate/blocks-react's <FmImage> writes it for you.
The SDKs
You rarely call these routes by hand:
@forumulate/embed/content—createContentClient({ library, key }):getEntries,getEntry,getGlobal,resolveRoute,query, cursor helpers, typedwhere, image helpers. Works whereverfetchdoes.@forumulate/next/content— the same reads tagged for Next's cache, draft-mode aware, pluscreateRevalidateHandlerand the preview routes.@forumulate/blocks-react—<Content>renders any entry of any type from its fields,<RichText>,<FmImage>; override any block or field with your own component.Astro, Nuxt and SvelteKit have receivers and catch-all helpers of their own; see SDKs and integrations.
Hosted output
A library can also be published without a front end of your own. Turn on its hosted output with PATCH /orgs/{orgId}/content/libraries/{libraryId} and { "settings": { "hostedOutput": { "enabled": true } } }. Turning it on needs the plan's content.hostedOutput feature (Starter and up), and a downgrade takes the hosted site dark again. While it is on, the platform renders the library's published, routed entries under the /h/ reader prefix, or at a custom domain you add to the library.
The reads behind that reader are public, and serve only published routed entries (never drafts or internal fields):
GET
/public/content/{orgId}/libraries/{libraryId}/hosted/resolve?path=/de/pricinganswers the page shown at a path, a language prefix included, or the redirect to follow.GET /public/content/{orgId}/libraries/{libraryId}/hosted/sitemap lists the routed, indexable paths with their last change and locale alternates.
Both answer 404 while hosted output is off or the plan no longer has it, the same 404 as a library that does not exist.
Next
Build a Next.js site on a content library: the catch-all route, revalidation, caching and preview.
Webhooks: the
content.*events and their payloads.API tokens, scopes and capabilities: the management tokens a migration script uses.
In the API
Every operation below is also reachable from the other surfaces without a dedicated command. forumulate api <Operation> calls it from the CLI, the JavaScript SDK has a typed method for each one (the last column names it; the Python and Go clients are generated from the same spec), and an agent reaches the organization-scoped ones through the MCP call_api tool. Where a CLI command or MCP tool exists too, the last column names it. Operation names link to the API reference.
Content Library
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| List libraries in an organization |
| |
| Create a library in an organization |
| |
| Get the library |
| |
| Update the library |
| |
| Delete the library |
| |
| List members in an organization (content libraries) |
| |
| Create a member in an organization (content libraries) |
| |
| Delete the member (content libraries) |
|
Content Model
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Get the model in an organization |
| |
| Replace the library's content model |
| |
| List versions in an organization (content libraries model) |
|
Content Studio
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Read the library's content types, resolved |
| |
| List a content type’s entries for the studio |
| |
| Publish, unpublish, delete or copy many entries |
|
Content Workflow
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Get the workflow in an organization |
| |
| Replace the workflow in an organization |
| |
| List change requests in an organization |
| |
| Create a change request in an organization |
| |
| Get the change request (content libraries) |
| |
| Update the change request |
| |
| Create the transition in an organization |
|
Delivery Entries
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| List the content types (libraries) |
| |
| List the content types (k) |
| |
| List entries of a content type (libraries) |
| |
| List entries of a content type (k) |
| |
| Query entries of a content type (libraries) |
| |
| Query entries of a content type (k) |
| |
| Read one entry (libraries) |
| |
| Read one entry (k) |
| |
| Resolve an unsaved draft |
| |
| Read a global (libraries) |
| |
| Read a global (k) |
| |
| Read the library’s JSON Schema (libraries) |
| |
| Read the library’s JSON Schema (k) |
| |
| Read the library’s OpenAPI document (libraries) |
| |
| Read the library’s OpenAPI document (k) |
|
Delivery Key
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Describe the calling delivery key |
| |
| Describe the calling publishable key by its id |
|
Delivery Keys
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| List a content library’s delivery keys |
| |
| Create a delivery key for a content library |
| |
| Update a delivery key |
| |
| Revoke a delivery key |
| |
| Rotate a delivery key |
|
Delivery Site
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| List routes (libraries) |
| |
| List routes (k) |
| |
| Resolve a site path (libraries) |
| |
| Resolve a site path (k) |
| |
| List redirects (libraries) |
| |
| List redirects (k) |
| |
| Read the sitemap (libraries) |
| |
| Read the sitemap (k) |
| |
| Search entries (libraries) |
| |
| Search entries (k) |
|
Entry Card
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Read an entry's status, schedule and locales |
| |
| Unpublish an entry |
| |
| Get the schedule in an organization (content libraries entries) |
| |
| Schedule an entry to publish |
| |
| Delete the schedule in an organization |
| |
| List an entry's revisions |
| |
| Read one revision's field values |
| |
| Restore a revision |
| |
| Start a translation of an entry |
|
Entry Delivery Preview
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Preview what delivery serves for an entry |
| |
| Resolve an entry’s unsaved draft for live preview |
|
Entry Fields
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| List fields in an organization (entries) |
| |
| Update fields in an organization (entries) |
| |
| Lock the entry |
| |
| Lock the entry (create) |
| |
| Lock the entry (delete) |
| |
| Create the heartbeat in an organization (content libraries entries lock) |
|
Entry Usage
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| List the entries that reference an entry |
|
Environments
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| List a library's sandboxes |
| |
| Fork a library into a sandbox |
| |
| Compare a sandbox’s model with its source |
| |
| Promote a sandbox’s model into its source |
|
Library Asset
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| List the assets a library’s media fields can use |
|
Library Item
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Create an item in an organization |
| |
| Create the lookup in an organization |
| |
| Get the item (content libraries) |
| |
| Update the item (content libraries) |
| |
| Delete the item (content libraries) |
| |
| List fields in an organization (items) |
| |
| Update fields in an organization (items) |
| |
| Publish the item (content libraries) |
| |
| List trash libraries in an organization |
| |
| Restore the trash |
|
Library Media
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| List a content library’s media |
| |
| Upload a file to a content library |
| |
| Update many of a content library’s assets at once |
| |
| Suggest alt text for a content library’s pictures |
| |
| Update one content library asset |
| |
| Delete a content library asset |
| |
| List the entries that use a content library asset |
|
Library Usage
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Read a library's usage against its plan |
|
Model Io
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Download the content model as a file |
| |
| Push a content model file (diff, dry run, apply) |
| |
| The library's delivery JSON Schema |
|
Preview Status
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Read a content library’s preview readiness |
|
Reference Candidates
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Search what a reference field can point at |
|
Releases
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| List releases |
| |
| Create a release |
| |
| Find drafts to add to a release |
| |
| Get a release |
| |
| Rename a release |
| |
| Delete a release |
| |
| Add a draft to a release |
| |
| Remove an item from a release |
| |
| Schedule a release |
| |
| Unschedule a release |
| |
| Publish a release now |
| |
| Roll a release back |
| |
| Preview a release |
|