Legal spaces
The content model
A legal document is a page in a Legal space. That is the whole model, and it has one consequence worth saying plainly before anything else:
Publishing a page in a Legal space publishes it as one of your organization's legal documents, on your org-wide legal index, immediately.
The page's slug is the document's address (/legal/{org}/privacy), the page's title is the document's title, and the page's declared fields are the document's facts. There is no separate "register this document" step, because two copies of the same fact drift — and they used to: you could publish a document and not see it, because a listener had not caught up.
Field | Type | What it does |
|---|---|---|
| text | What you cite when somebody asks which terms they agreed to. |
| date | Shown to readers. A date in the future schedules the edition you publish. |
| date | When the text last changed. Shown beside the version. |
| text | One or two sentences on what this edition changed. Readers see it in the history and when they are asked to accept again. |
| text | Governing law. |
| text | The version this one replaces. |
| date | Internal. Counsel's own reminder; never sent to a reader. On the day, a reminder is mailed (see Review-by reminders). |
| boolean | Members are prompted to accept it, and prompted again whenever it changes. |
| boolean | Keep this page off the legal index. The page stays where it is, with its history and its own address; it just stops being one of your legal documents. |
Region and language versions add variantOf, region, language and informational (a translation for information only, which names the version that governs).
An edition is a version of a document, pinned to the page revision it was, so "what changed since the version you accepted" is answerable. Editions are pinned when a page is first mirrored, when a page is bound, and on every re-publish.
How to fill it by API
# 1. The space
POST /orgs/{org}/spaces
{ "name": "Legal", "slug": "legal", "type": "LEGAL", "visibility": "PUBLIC" }
# 2. A document (collection id from GET /orgs/{org}/spaces/{space}/collections)
POST /orgs/{org}/spaces/{space}/collections/{collection}/pages
{ "title": "Privacy Policy", "slug": "privacy" }
# 3. The text
PUT /orgs/{org}/spaces/{space}/pages/{page}/content
{ "title": "Privacy Policy", "content": { … block doc … } }
# 4. Its facts
PATCH /orgs/{org}/spaces/{space}/pages/{page}/metadata
{ "fields": { "version": "2026-08-01", "effective": "2026-08-01", "mustAccept": true } }
# 5. Publish. It is now a legal document.
POST /orgs/{org}/spaces/{space}/pages/{page}/publishdocs/import works here too: one markdown document per legal document.
Binding a page that lives somewhere else
POST /orgs/{org}/legal/bind { "kind": "privacy", "pageId": "…", "required": true }Binding says "this page plays this role" for a page that is not in the Legal space (or is, but should answer at a different address than its slug). What it changes, concretely:
The document appears on the org's legal index at
kind, i.e. at/legal/{org}/{kind}, and inGET /public/orgs/{org}/legal.An edition is pinned, so
…/versionsand…/diffanswer about it.required(and the page's ownmustAcceptfield, which wins afterwards) decides whether members are prompted to accept it on their next visit. By itself it gates nothing for readers; what a reader is kept from until they accept is the space's legal gate (noneby default), described in Binding a legal document, and whatrequiredreally does.Pointing an existing
kindat a different page is a new edition, so the version bumps and everyone is asked again.Binding clears
unlisted: naming a page as a legal document puts it back on the index.
The page itself keeps its own space, history and review. The page must be published first — nobody can accept a draft (422).
Removing a document
DELETE /orgs/{org}/legal/{kind}A
kindthat is not a document answers 404.A page-backed document is the page, so this sets that page's
unlistedfield and leaves the row (which is what holds the acceptance history). The page stops being a legal document; nothing else about it changes. Clearunlisted, or bind it again, to put it back.An inline document (see below) is the row, so this deletes it.
Deleting the page, or the whole Legal space, also takes its documents off the index — public and admin alike.
Inline documents
POST /orgs/{org}/legal { kind, title, body, required } writes a document as Markdown on a row, with no page behind it. It predates documents being pages and is kept for the orgs that have them.
Inline documents do have editions. Every upsert pins the body it published as that edition, so …/versions lists them and …/diff?from=N compares them, exactly as it does for a page-backed document. (An earlier version of this guide said the opposite — that an inline document's versions was always empty — which was true of the behaviour it described and has not been since the edition is pinned on upsert.)
Authoring in a Legal space is still the better path: a page gets review, change requests and the editor.
Acceptance
Two separate registers, for two different populations.
Members of your organization — driven by mustAccept:
GET /orgs/{org}/legal # documents + what this member still owes
POST /orgs/{org}/legal/{documentId}/accept # record this member's acceptanceYour own end users, through the embedded document — the embed renders at /legal/{org}/{kind}?embed=1&subject=<your user id> and posts:
POST /public/orgs/{org}/legal/{kind}/accept
{ "subject": "user-42", "subjectToken": "<hex hmac>" }
GET /orgs/{org}/legal/{kind}/acceptances # the register
GET /orgs/{org}/legal/{kind}/acceptances.csv # the same, as a filesubject is your identifier for your user — opaque to us. We are your record-keeper here, not your authenticator, which makes the next part the important part.
Signing subjects (do this if the register is evidence)
By default this endpoint takes no credential, so a register records claims: anyone who can reach the page could post any subject. Both the JSON and the CSV say so — subjectVerification: "unverified".
Set a secret and the register records evidence instead:
PUT /orgs/{org}/legal/acceptance { "sharedSecret": "<32+ characters>" }
GET /orgs/{org}/legal/acceptance → { "requireSignedSubject": true }From then on every acceptance must carry subjectToken, and an unsigned one is refused with 403 rather than quietly filed. The token is a hex HMAC-SHA256 over <orgId>:<kind>:<subject>, signed with the secret:
crypto.createHmac('sha256', SECRET).update(`${orgId}:${kind}:${subject}`).digest('hex');Mint it on your server, next to wherever you decide which user is looking at the page. The secret is write-only — it is never returned by this endpoint or by any space payload. { "sharedSecret": null } clears it and the register goes back to recording claims. Rotating it invalidates tokens minted with the old one.
Each row also carries the accepting client's real IP and user-agent, taken from the edge headers (CF-Connecting-IP, then X-Real-IP, then X-Forwarded-For) rather than from the connection, which behind our proxy is the same internal address for every caller on earth.
What readers see
An index of your documents at
/legal/{org}— unless you choose the "No front door" preset, in which case there is no index and each document is only its own address, linked from wherever you use it.A document at
/legal/{org}/{kind}, in your site's chrome, with its version, effective and amended dates and governing law above the text.Other languages. Translate a policy either way, or both:
Locale twins, as on a docs space:
POST /orgs/{org}/spaces/{space}/docs/locales{"code":"es","label":"Español"}clones every document into the language as a draft at the same slug; publish the Spanish text and it is served at the document's Spanish address.GET /public/orgs/{org}/legal/{kind}?locale=esreturns it withlocale(the language served) andlocales(every declared language,availablewhere this document is published in it). The edition, the facts and acceptance stay the original's — a twin is the same document in other words, never a separate thing to accept.Region / language versions (
terms-dewithvariantOf: terms): their own pages with their own editions — use these when a translation is a different legal text.
The document shows ONE language control listing both (
theme.legal.variants.picker, ortheme.docs.localeSwitcher—style,label— on the LEGAL space). A language that only a version has goes to that version's address. A declared language this document is not translated into is a 404, or the original under a "not translated yet" notice withtheme.docs.localeFallback: "source".The site's
customJsruns here as on every other page of the site.reviewByis never shown.WebPagestructured data.?embed=1&subject=…renders the document bare, for an iframe, with an "I accept" button that posts the acceptance above and thenpostMessagesforumulate:legal-acceptedto the parent frame.
Front page
The index is a composed document. Until you edit it, it is generated: the intro band, then compliance, documents, access and status. The trust-center bands draw nothing until you give them content (see Trust center), so a plain register still reads as an intro over its documents. Under the "No front door" preset nothing is generated — but a front page you store still gives the space one. A legal space has no list a spaceFeed can read, so a feed pointed at one renders nothing.
How the register and documents look
Everything about how a Legal space reads lives in theme.legal, set with the space theme (PATCH /orgs/{orgId}/spaces/{spaceId}/theme). Like every theme key, a misspelt one is a 422 that names its path. The layering is the usual one: the look's patch, then your space theme, then a document's own customization.
Looks are patches
theme.legal.look picks a starting point. A look is only a patch of independent keys over the default layer. It is never a separate reader, so every key below works under every look, and a key you set always wins over the look's.
Look | What it sets |
|---|---|
|
|
| Nothing. The reader exactly as it rendered before Legal spaces had a theme of their own. |
| The default's keys, plus |
| The default's keys, plus |
| The default's keys, plus a compact ruled |
Because each look is just keys, you can combine parts of different looks. Some examples:
{ "legal": { "look": "trust-center", "numbering": { "style": "decimal", "levels": 2 }, "gloss": "margin" } }The trust-center register and badges, with the ledger's numbered clauses and margin glosses.
{ "legal": { "look": "plain-site", "history": "disclosure", "summaryLook": { "tone": "brand" } } }GitHub-style ruled register and glossary table, with the history closed and a brand-tinted plain-English box.
{ "legal": { "look": "ledger", "docNav": "none", "register": { "divider": "gap", "columns": 2 } } }The ledger's document setting without its sidebar, over a gapped register.
The register
The register's arrangement is the space's layout preset: Document list (standard), Heading beside list (split), Cards (cards) or No front door (no-front-door). The keys under theme.legal.register are the parts every preset shares:
Key | Default | What it does |
|---|---|---|
|
|
|
| off | Each group becomes a page of its own at |
|
| The line under each title: the page's search description, else the first sentence of its summary. |
|
| Which date a row prints: |
| shown | "Edition 3" beside the date. |
| on under | A "Read" button on each row. |
|
| A filter box: |
|
|
|
| 2 | Cards to a row under |
|
|
|
| a hairline |
|
|
|
|
| — | A card's |
| shown | The register's heading: |
| as written |
|
| the reader's own words | What the register says when nothing is published, or when the filter matches nothing. |
theme.legal.list also takes the shared index keys (layout, grouping, facets), for registers drawn through the generic list.
The register publishes CollectionPage and ItemList structured data.
A document
Key | Default | What it does |
|---|---|---|
|
| What the line under the title leads with: "Effective … · Last updated …", or the edition counter first. |
|
| A small upper-case line above the title: |
| on | The kicker in the monospace face. |
| on | The 2px rule above the title. |
|
| The kicker's colour: |
| the docs head's size |
|
| the browser's short date (default look: | Every date on the register and the document, spelled one way: |
| — | The facts strip under the head (effective date, governing law…), in the shared facts look. |
|
| Automatic clause numbers, written into the headings: |
|
| The "In plain English" section, in a tinted box. |
| — | That box's |
|
| The edition history under the head: a closed "History" control, |
|
| The notice over a document with a new edition scheduled. |
|
| That notice as a full |
|
| The other documents beside this one: a left |
|
| The same on a phone: |
|
| Where a |
|
| The Definitions section as a |
| every allowed action, as a row | Copy link, Markdown, print or PDF, share; |
| inline, everything shown | "Compare editions": |
| off | A "Get updates" control on the document itself, not only on the register. |
|
| Region and language versions: |
| the head, no facts | A document framed with |
| a dismissible banner | How members are asked to accept. See Binding a legal document. |
| off | A public comment period on upcoming editions (below). |
| — | The cookie declaration table (below). |
| — | The trust-center bands and the sub-processor table: see Trust center. |
Every document page can be printed or saved as PDF, and archived and upcoming editions use the same layout as the edition in force. A past edition that was never recorded says so, rather than showing an empty page.
One document's own look
A document can override theme.legal (and theme.docs) for itself. A page in a Legal space takes it in its metadata:
PATCH /orgs/{org}/spaces/{space}/pages/{page}/metadata
{ "theme": { "legal": { "numbering": { "style": "decimal", "levels": 3 }, "docNav": "none" } } }An inline document takes the same thing under customization.theme. Any other type's namespace (theme.forum, theme.careers…) is refused with a 422 that names it (customization.theme.forum) and the namespaces this piece can take. null clears a key. In the studio this is Page look & sharing in the document's details.
A document in a second Legal space wears the register's theme.legal under its own space's theme.
Addresses
Address | What it is |
|---|---|
| The register: the organization's first public Legal space. |
| A document. |
| An archived edition; |
| The edition in force as Markdown (never the upcoming one). |
| The document's sub-processor table as data. |
| A group's page, with |
| Another Legal space's own register: a second trust center. |
| New editions, as feeds. |
| The script embed (below). |
The register's search is GET /public/orgs/{orgId}/spaces/{spaceId}/legal/search (the site header's search uses it), and readers can subscribe to editions by email with POST /public/orgs/{orgId}/spaces/{spaceId}/legal/subscribe (reference). scope.kinds narrows a subscription to named documents.
Embedding a policy on your own site
One tag in your site's footer:
<!-- the register's documents, as a list of links -->
<script src="https://acme.com/legal/embed.js"></script>
<!-- one document, framed and sized to its height -->
<script src="https://acme.com/legal/embed.js" data-document="privacy"></script>The list is written into the script on our server, so your page makes no request back to us and nothing third-party loads on either side. Each link opens the register's own address. A framed document keeps its accept control and its change notes, and theme.legal.embed controls what it leaves out.
Comment periods on upcoming editions
With theme.legal.comments: true, a scheduled edition (one whose effective date is still in the future) is open for comments at /legal/{org}/{kind}/v/next. Nothing shows until you publish it, optionally with an answer. Comments close when the edition takes effect.
GET /public/orgs/{orgId}/trust/editions/{kind}/next/comments # the published ones
POST /public/orgs/{orgId}/trust/editions/{kind}/next/comments { name, email, body }
GET /orgs/{orgId}/trust/comments # the queue
POST /orgs/{orgId}/trust/comments/{id}/moderate { "status": "published", "reply": "…" }In the studio this is Edition comments.
Writing policies
The policy generator and clause library
A template, not legal advice. Every policy the generator drafts says so at the top, and so does the generator itself. Review the text with your own counsel before you publish it or rely on it, and remove the note once they have.
Generate a policy in the studio drafts a new document from a short questionnaire. It can draft six kinds: privacy, cookie, terms, EULA, returns and disclaimer. Every kind asks for the company name, website, contact email and governing law. Each kind then asks its own questions, for example whether people create accounts, whether you measure site use, take payments, or have users in the EU or California.
Your answers switch on the matching clauses from a library of 34 plain-language starting clauses. You can add or drop any of them before creating the draft. A blank you left unanswered reads as a plain phrase ("the company"), never as a bare placeholder. The result is an ordinary draft page in the space, with its summary, its text and its effective and jurisdiction fields filled in. You edit it, review it and publish it like any other document. Nothing is published by itself.
By API:
POST /orgs/{orgId}/spaces/{spaceId}/legal/policies
{ "kind": "privacy",
"answers": { "company": "Acme Ltd", "website": "acme.com", "email": "privacy@acme.com", "gdpr": true },
"clauseIds": ["privacy.who", "privacy.sharing", "privacy.gdpr"] }
# → { "page": { "id", "slug", "title" }, "generationId" }We maintain the clauses
Forumulate maintains the clause library. Each clause has a version and the date our staff last reviewed it, and every clause is reviewed every quarter. When the law or good practice moves, we change the clause and bump its version.
The generator records which version of each clause your policy used. When a clause you used changes:
Your organization's owners and admins are told in the app and by email ("A clause in your generated policy was updated").
The studio shows the change at the top of the Legal space (Documents and Generate a policy): which clause, from which version to which, what changed, and a word-by-word diff of your text, with your answers filled in.
Update my policy regenerates the document from your original answers and clause choice at the current versions. If the document is live, it is published as a new edition through the ordinary legal versioning, with
amendedset to today andchangesnaming the clauses, so readers see what changed and, wheremustAcceptis on, are asked to accept again. A document that is still a draft stays a draft.If you edited the document after it was generated, Update my policy asks before it replaces those edits (the API answers
409unless you sendforce: true). You can also apply the change by hand and choose Not now.
GET /orgs/{orgId}/legal/clause-updates # what your policies have not taken yet
POST /orgs/{orgId}/legal/clause-updates/{generationId}/apply { "force": false }
POST /orgs/{orgId}/legal/clause-updates/{generationId}/dismissOnly policies created with the generator from 20 October 2026 are tracked; a document you wrote yourself, or generated before then, is yours to keep up to date. Updated clauses are still a template, not legal advice: review the change with your counsel.
Review-by reminders
A document's internal reviewBy date now sends a reminder (the legal.review-due email template) on the day it arrives. The reminder goes to the document's contact field when that holds an email address, else to your organization's support address. It is sent once per document and date, so moving the date sets a new reminder.
The cookie declaration
theme.legal.cookies sets a cookie table above the text of one document:
Key | Default | What it does |
|---|---|---|
| — | The document's address ( |
|
|
|
| — | Up to 100 rows of |
The generated half lists the cookies the platform itself may set, plus the ones set by each analytics vendor the space's analytics settings turn on (GA4, Google Tag Manager, Segment, Intercom, Matomo, PostHog). Rows are grouped in the same categories your consent banner uses, so the policy and the banner agree.
The Cookie scanner in the studio reads one https:// page you name (your marketing site, your app's sign-in page). It reports every cookie the page sets and the cookies its known third-party scripts set, and you add the rows you want to keep. It cannot see a script that another script loads later.
POST /orgs/{orgId}/trust/cookie-scan { "url": "https://acme.com" }Outlets
Theme components and extensions can add markup in these slots on the legal surface:
Outlet | Pages | Context | Where |
|---|---|---|---|
| register, group page | — | Above the list of documents. |
| register, group page | — | Below the list. |
| document, edition |
| In the header, beside the title and dates. |
| document, edition | the same | Under the title and dates, above the text. |
| document, edition | the same | In the side column, under the section navigation. |
| document, edition | the same | After the text, above the version history. |
None of them wraps reader content, so replace behaves as after.
In the studio
The Legal space's studio has these sections:
Documents: the binder, with an inline "New document".
Acceptances & editions: who accepted which edition, each edition's note, and what changed.
Signed acceptance: the acceptance secret, and binding an existing page as a document.
Access requests, Privacy requests: the trust-center queues (Trust center).
Edition comments: the comment-period queue.
Generate a policy, Cookie scanner, Security questionnaires.
Design: every
theme.legalkey, by the part of the page it shapes. Link fields are checked as you type. Image fields (a badge's logo) upload into the space's library.
What you need to set up
Feature | What it needs | Until then |
|---|---|---|
Review-by reminders | A support address in Settings, or an email address in the document's | No reminder is sent. |
Edition-change emails, subscriptions | Your organization's mail settings. | The feeds still carry every edition. |
Security questionnaires | Your own AI provider (bring your own key), connected in Settings. | The panel says where to connect one. |
Trust-center integrations | — |
Published. https://forumulate.com/guides/legal-spaces, in the
guidesspace's "Building your site" collection. Re-import after editing — the page is not generated from this file.
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.
Legal Policy
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Generate a policy from the template |
| |
| List clause updates for your generated policies |
| |
| Update my policy to the current clauses |
| |
| Dismiss clause updates for a generated policy |
|
Legal Search
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Search a legal register's published documents |
|
Legal Updates
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Subscribe the legal |
|
Org legal
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Get the legal in an organization |
| |
| Create the legal in an organization |
| |
| List acceptances in an organization |
| |
| Get the acceptance in an organization |
| |
| Replace the acceptance in an organization |
| |
| Get the diff in an organization |
| |
| Replace the edition |
| |
| File of acceptances |
| |
| Bind the legal |
| |
| Delete the legal |
| |
| Get the suggested in an organization |
| |
| Reorder the legal |
| |
| Accept the legal |
|