Page passwords and head code
Two per-page settings that site builders like Webflow, Framer and Carrd have, and that pages read by our page reader (documentation, FAQ, careers and custom spaces) now have too:
a password on one page, for a partner page, a press kit or a draft you want to show a few people;
head code on one page: a verification tag, a stylesheet, or a script that belongs on that page only.
Both are in the page's details panel in the editor, and both need the space.update permission, not just permission to write the page. A password changes who can read a page, and head code is code that runs on it. Someone who may draft a page should not be able to do either.
A password on one page
In the page's details panel, open Password, type a password of six characters or more, and Lock the page. You can add a line for the lock screen ("For partners: ask your account manager").
curl -X PUT "https://forumulate.com/api/orgs/$ORG/spaces/$SPACE/pages/$PAGE/access" \
-H "Authorization: Bearer $FORUMULATE_TOKEN" -H "Content-Type: application/json" \
-d '{ "password": "harbour-lights-42", "prompt": "For partners: ask your account manager." }'Field | What it does |
|---|---|
| 6–200 characters. |
| The line on the lock screen, up to 300 characters. Optional. |
GET …/pages/{pageId}/access answers { "access": { "locked", "prompt", "updatedAt" } }. It never returns the password, and nothing else does either.
What a reader sees
A reader who opens a locked page gets the lock screen: your prompt, a password box, and nothing of the page. The API answers the page with 401 and only the prompt, and it checks the lock before it reads the page's body, so no part of the text, its fields, its markdown or its PDF leaves the server. The lock screen is kept out of search engines.
The right password sets an unlock cookie for that page, and the page opens. The reader stays in for 12 hours, on that browser.
POST /public/orgs/{orgId}/spaces/{spaceId}/pages/{pageId}/unlock { "password": "…" }
→ 200 { "ok": true } and the unlock cookie, or 401 "That password is not right."Changing the password signs everyone out. Each lock has a version that goes up with every change, and the unlock is tied to the version, so a new password ends every earlier unlock at once. Removing the lock opens the page to anyone who can read the space.
What stays hidden, and what does not
A locked page is left out of every place that would otherwise carry its text: search, the llms.txt and llms-full.txt files, feeds, the space export and PDF, and the MCP servers.
Its title stays in the navigation, as it does on Webflow. To hide the page from the navigation as well, hide it there.
A password protects one page from people who do not have it. It is not an account: everyone who knows the password gets the same access, and there is no record of who unlocked. For named people, use reader sign-in and groups (Reader access).
How it is protected
The password is hashed with argon2id, with the same settings as account passwords. The hash is kept in its own table, isolated per organization and per space, never in the page's metadata. The page itself only carries "locked" and the prompt.
Guessing is limited. Ten wrong tries per page from one network in 15 minutes, then
429"Too many tries". The unlock route is also throttled for everyone (20 a minute per network). A wrong guess and a guess at a page with no lock take the same time, so timing gives nothing away.The unlock token is signed by us over the page, the lock's version and an expiry, and checked in constant time.
The cookie is HttpOnly (page scripts cannot read it), SameSite=Lax and Secure. It is named
cf_share_pg_<page>, and anycf_share_cookie tells our web tier and the edge not to cache the response, so an unlocked page is never stored in a shared cache and served to someone else.
Head code on one page
In the page's details panel, Head code takes up to 20,000 characters for that page's <head>. It is page metadata, customHead:
curl -X PATCH "https://forumulate.com/api/orgs/$ORG/spaces/$SPACE/pages/$PAGE/metadata" \
-H "Authorization: Bearer $FORUMULATE_TOKEN" -H "Content-Type: application/json" \
-d '{ "customHead": "<meta name=\"google-site-verification\" content=\"abc123\">" }'Writing customHead without space.update answers 403. An empty string removes it.
It follows the same rules as the space's own head code (the theme's customHead), applied to one page.
What you can put there
The code is parsed, not pasted. Only three tags are kept:
Tag | Kept | Where it works |
|---|---|---|
| with | Every address. |
| with a | Every address. |
|
| Only on your verified custom domain, from allowed origins, after consent. |
Everything else is dropped: <style>, <base>, <iframe>, <object>, <embed>, any other attribute, and a <link> or <script src> whose address is javascript: or data:. A snippet with no tags at all is treated as JavaScript, which is what older snippets were.
That covers what people usually need on one page: a search-console or domain-verification tag, Open Graph or app tags, a stylesheet or font, a preconnect, an icon, and on your own domain a page-specific script such as an ad conversion tag or a calculator.
Why scripts run only on your own domain
On forumulate.com, every organization's pages share one origin with our /api. A script there would run with the reader's Forumulate session, and one tenant's page could reach another tenant's data. So on our host only the inert tags (<meta>, <link>) are emitted, and the script part of your head code is never even sent to the browser. On a custom domain you have verified (Custom domains), your pages are your own origin and scripts run.
Even there, three rules hold:
Inline code runs under the page's security policy, with the nonce we issue for that request. It cannot be injected from elsewhere.
<script src>loads only from origins the space allows,theme.scriptOrigins(the studio's Scripts & ads panel). A page's head code cannot add an origin of its own. Add the vendor there first.Scripts wait for consent. If your site asks readers for cookie consent, head-code scripts wait until the reader accepts the functional category, exactly like the space's own head code.
Why http-equiv is refused: it is how a <meta> sets a security policy or redirects the page, which is control of the page rather than decoration. <base> would change where every link on the page points, and an <iframe> is not head content. The preload family of <link> loads code rather than describing the page.
See Advanced theming for the space-wide script rules and how to read the allowlist a reader gets.
What you need to set up
A password needs nothing beyond
space.update.Head code with scripts needs a verified custom domain for the space and the script's origin in Scripts & ads (
theme.scriptOrigins). Until then, readers get the<meta>and<link>tags only, and the scripts do not run on our host.
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.
Public Page Unlock
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Unlock the page |
|