Binding a legal document, and what required really does
Legal spaces covers the content model: a page in a Legal space is a legal document, the eight declared fields, editions, inline documents, and both acceptance registers. This page answers the two questions that page leaves open — what legal/bind records, and what required does to a reader.
The short answer to the second one is: nothing, until you turn on the space's legal gate. Read on before you rely on it.
Binding a page to a kind
POST /orgs/{orgId}/legal/bind
{
"kind": "privacy",
"pageId": "b0662f97-…",
"required": true
}Field | |
|---|---|
| 1–40 characters. The stable slug the document answers on — |
| A published page in a Legal space in this organization. |
| Optional, defaults to |
Answers { "document": { … } }.
It needs the admin token scope
@RequireCapability('org.manage_settings')
@RequireScope('admin')A read/write token cannot bind, unbind, or set the acceptance secret, even with org.manage_settings. This is not stated anywhere else and it is a real integration blocker: the call answers 403 with a token that looks sufficient. Mint a token with "scopes": ["read", "write", "admin"] for this one.
What it refuses
Status | Message |
|---|---|
422 |
|
404 | The page is not in this organization. |
403 | The token lacks the |
What it records
Two rows:
A legal document row — the
kind, the title copied from the page's live revision, thepageId, aposition, aversion, andrequired.An edition row pinning
{ kind, version, revisionId }— the page revision that this version is.
So binding records a version number and the exact page revision behind it. It records no user, no IP and no timestamp of who bound it beyond the row's updatedAt.
Two side effects worth knowing:
Binding clears
unlisted. A document you bound is a document you are publishing.Re-pointing a
kindat a different page bumps the version, which means everybody re-accepts.
How a new version is triggered
Three paths can increment the document's version. Two of them do it only when the words actually changed:
What you do | Result |
|---|---|
Publish a bound page | The version bumps and a new edition is recorded only if the published title or text differs from the edition in force. An identical republish keeps the version, and nobody is asked to accept again |
Re-point a | Version bumps. |
Edit an inline document's title or body | Version bumps, but only if the title or body actually changed. Sending the same body again is a no-op |
Acceptance is then stale automatically, because "accepted" compares the stored acceptance version against the document's current version.
Republishing the same text
Publishing a page whose draft says exactly what the live revision already says succeeds as usual (it clears the draft) and is reported, not refused. The publish response carries unchanged: true:
{ "revision": { "id": "…", "status": "PUBLISHED", "unchanged": true } }On a bound page that means the legal version is unchanged, no edition is minted, and every reader's existing acceptance still counts. A routine re-import or a habitual "publish" therefore does not re-prompt anybody.
The two checks are not identical. unchanged on the response compares the whole revision, so a changed image or block attribute makes it false. The legal check compares only the title and the words a reader agrees to, so a republish that changes formatting but not wording can answer unchanged: false and still keep the legal version. Neither check ever makes the publish fail.
The page metadata field mustAccept syncs to the row's required. It is a presence check on the metadata payload, so a metadata update that does not mention mustAccept leaves required alone.
required: what it actually does
On its own, required marks a document as one that asks to be accepted, and nothing is withheld. What a reader is kept from until they accept is a separate setting per space, the legal gate, and its default is none.
GET of the acceptance status returns:
{
"documents": [ { "kind": "privacy", "version": 3, "required": true, "accepted": false }, … ],
"pending": ["privacy"]
}pending is the list of kinds that are required and not accepted at the current version. In the member console it drives a dismissible banner, not a modal: a hard modal would mean a mis-published document could lock an organization out of its own console.
How the member prompt asks
The Legal space's theme.legal.gate decides how that prompt asks:
Key | Default | What it does |
|---|---|---|
|
|
|
|
| What the prompt opens on: the |
| on |
|
PATCH /orgs/{orgId}/spaces/{legalSpaceId}/theme
{ "legal": { "gate": { "style": "dialog", "opensOn": "changes", "dismissible": false } } }With dismissible: false the prompt stays on screen until the member accepts. That is why the default can be put off: if a document is published by mistake, every member is asked about it at once.
The public document shape carries required too, so a reader surface can tell a mandatory document from an optional one.
Making a reader accept
Set the space's legal gate (PUT /orgs/{orgId}/spaces/{spaceId}/legal-gate, or the organization default legal.require_acceptance). post withholds posting, commenting, voting and applying; read withholds the space's content; signin withholds the session until the reader accepts. The values, and what happens to a reader who has not signed in, are in Who may read, sign up and sign in to a space. A refused request is a 403 with the code legal.acceptance_required, naming the documents to accept in details.documents.
A reader surface renders the acceptance step from two public calls, which need no token:
GET /public/orgs/{orgId}/spaces/{spaceId}/legal-gateanswers the resolvedgate, thedocumentsthis reader has not accepted at the current edition (empty when nothing is pending or the gate isnone), andacceptor: how much the reader's identity is worth (identifiedoranonymous), ornullwhen there is nobody to record an acceptance against. The reader's subject itself is never returned.POST /public/orgs/{orgId}/spaces/{spaceId}/legal-gate/{kind}/accept records the acceptance of one document at its current edition and answers
{ accepted: true, kind, version, verification }. There is no subject in the body: we decide who is reading. A reader we cannot identify is a 401 underrequire_identity; undercookiea pseudonymous cookie is minted at this moment, never on a status read. A kind the organization does not require is a 404. Accepting twice at the same edition records one row.
These acceptances go into the subject register below with source: 'reader', and because a legal document belongs to the organization, accepting from one space counts in every space. The status call is limited to 60 requests a minute per client and the accept call to 30.
You can also gate in your own application instead: call GET /public/orgs/{orgId}/legal/{kind}/versions for the current version, compare it with what you have recorded for that user, show your own interstitial, and record the acceptance with POST /public/orgs/{orgId}/legal/{kind}/accept.
The two acceptance registers are not the same
They record different evidence, and this matters if somebody asks you to produce proof.
Member register | Subject / embed register | |
|---|---|---|
Who | A signed-in member, by user id | Any subject string you supply |
Version | yes | yes |
Timestamp | yes | yes |
IP address | no | yes |
User agent | no | yes |
Uniqueness | one row per (document, user, version) | duplicates ignored |
No IP or user agent is captured for a member acceptance. If your legal advice is that acceptance evidence needs an IP, the member register does not provide it and you should record acceptance through the subject register instead, with your own subject identifier.
The public endpoints
Call | Returns | Rate limit |
|---|---|---|
|
| 120/min |
|
| 120/min |
|
| 120/min |
|
| 30/min |
A public document carries kind, title, body (inline markdown or null), content (a block document or null — exactly one of the two is set), pageId, spaceId, fields, required, version and updatedAt.
Two filters that explain a document "missing" from the index:
A document whose page is still a draft is absent entirely. Published means it has a body or a block document; a draft has neither.
Internal fields are stripped.
reviewBynever reaches the wire.
verification on an acceptance is "signed" when you supplied a valid subjectToken and "unverified" when no acceptance secret is configured. If a secret is configured and the token is missing or wrong, the call is a 403 whose message carries the HMAC recipe — the comparison is constant-time.
One correction to the Legal spaces page
The published Legal spaces page says:
Inline documents have no editions:
…/versionsreturns an empty list and…/diff404s, because there is no pinned revision to compare. Author in a Legal space instead.
That is no longer true. An inline document records an edition on every content change, so …/versions lists its history. Editing an inline document's title or body bumps the version and writes an edition row just as a page-backed document does.
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 Public
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Get the legal in an organization (public reader) |
| |
| Accept the legal (public reader) |
| |
| List versions in an organization (public reader legal) |
| |
| Get the version |
| |
| Get the diff in an organization (public reader) |
| |
| RSS feed of the legal |
| |
| Atom feed of the legal |
| |
| JSON feed of the legal |
| |
| JSON feed of trust |
| |
| List subprocessors in an organization |
| |
| Get the legal |
|