Privacy, GDPR and data requests
This page is about the mechanics: which switch, which endpoint, which screen, for each thing a data-protection regime asks an organization to be able to do. It is not legal advice, and it is not our own privacy policy — that is at forumulate.com/policies/privacy and it is the document that actually binds us.
Who is responsible for what
For the people in your organization and the readers of your spaces, you are the controller: you decided to collect their words, their email addresses and their sign-ins. We are your processor, and we hold that data to run the service you asked for.
For your own staff account — your name, your email, your billing — we are the controller, and the policy linked above is the one that applies.
That split is the reason the two halves of this page look different. Your readers' requests come to you and you answer them with the tools below; your own request comes to us.
What is stored about a person
What | Where it comes from | How to see it |
|---|---|---|
Account | Sign-up, or the identity provider they signed in with |
|
Membership and role | An invitation you sent, or a reader sign-in | Members in the console |
What they wrote | Pages, posts, replies, comments, reactions | The space it is in |
Files they uploaded | The editor, or a reply | Files in the console |
Reader sign-in identity | Their provider's subject id, kept so the same person returns to the same member | Members → the member |
Consent receipts | Your CMP, if you run one |
|
Audit records | Staff actions, kept so a change can be attributed |
|
Page views and searches are recorded as aggregates by default, which is why a space's analytics can tell you what readers looked for without telling you who looked.
How a view is counted. A docs page or forum topic counts one view per reader per day. Crawlers, uptime and performance tools, HTTP libraries and browser prefetches are not counted. To tell one reader's repeat visits from another reader's first visit, the server keeps a keyed hash of the signed-in member, or of the address and browser for a signed-out reader. The hash cannot be turned back into either, and it is deleted after two days. A topic's view count is updated every few minutes, so a new view can take up to five minutes to appear.
Access requests (Article 15)
A person can export their own data without asking anybody:
GET /api/users/me/exportIt answers with everything that account holds across every organization it belongs to — profile, memberships, content, and the identifiers any linked provider sent. Account → Data & privacy in the console is the same export with a button on it.
For a whole organization, including every space and its content:
GET /api/orgs/{orgId}/exportThat one is an organization export, not a personal one: it is the right answer to "give us our data back", and the wrong answer to a subject access request about one reader.
Erasure (Article 17)
POST /api/users/me/deleteDeleting an account removes the account and the personal data attached to it. What a deletion does not silently do is tear the content it wrote out of a space other people are still reading — an answer in a forum thread is part of somebody else's conversation. Decide that deliberately for each case, and say in your own policy which way you go.
Audit records outlive the account on purpose. An audit log that can be erased by the person it describes is not an audit log, and every regime that asks you to keep one says so.
Automating requests end to end
If you run a privacy or consent platform that issues data-subject requests, Forumulate can act as an executor for it: the platform sends the request, we look the person up, run it, and hand back the result.
POST /api/orgs/{orgId}/privacy/executor-key # mint this organization's credential
GET /api/orgs/{orgId}/privacy/executor-key # is one configured?
POST /api/privacy/executor/{orgId}/lookup # does this person exist here?
POST /api/privacy/executor/{orgId}/requests # open a request
GET /api/privacy/executor/{orgId}/requests/{id} # poll it
GET /api/privacy/executor/{orgId}/requests/{id}/export # collect the resultThe credential is per organization, and the path carries the organization it belongs to. A key minted for one organization cannot read another — which is the whole point, and is why a key minted before this was scoped has to be re-minted.
Looking a person up
lookup takes the person's email address, the externalId your sign-in provider issued them, or both:
{ "email": "ana@example.com" }
{ "externalId": "user_8812", "externalProvider": "external_jwt" }It finds every member of the organization however they joined: the owner and invited staff, and readers who signed in through a token from your app (external_jwt), OIDC, a social provider or a password — including a reader who signed in for the first time a moment ago. The address is matched without regard to case. externalId is the value GET /orgs/{orgId}/members reports for that member; externalProvider is optional and narrows the match to one sign-in method. The answer is {"subjectId": "…"} for a member and {"subjectId": null} for anyone else — somebody unknown, and somebody who has an account but is not a member of this organization, read the same.
Minting the credential again retires the previous one on the very next request.
Poll the request until it reports done, then follow the resultUrl it carries. A poll that ignores resultUrl and guesses an address will not find the file.
Receiving your CMP's data-subject requests by webhook
If your consent platform pushes events rather than calling an executor, point its webhook subscription at POST /hooks/consent/{orgId} (served under /api on the public origin). Any CMP works: nothing in the address names a vendor.
Mint the signing secret with
POST /orgs/{orgId}/consent/webhook-secret(needsorg.manage_settings). It answers{ secret, webhookUrl }, with awhsec_secret shown once; give both to your CMP. You can instead store your CMP's own secret (8 to 300 characters) aswebhookSecreton PUT /orgs/{orgId}/consent.Your CMP signs each delivery with HMAC-SHA256 over the raw body, as hex or base64, optionally
sha256=prefixed, inX-Consent-Signature(orX-Webhook-Signature, or CookieMunch's own header). When it also sendsX-Consent-Timestamp, a signature over{timestamp}.{body}is accepted too.POST /orgs/{orgId}/consent/webhook-testsends a correctly signed test event through the same path, so you can confirm the loop before a real one arrives.
A delivery with a bad or missing signature is a 401, and an organization with no secret answers 404. A verified delivery answers { "ok": true } and records when the last event arrived. This webhook carries consent events for your organization only. A dsar.created delivery is refused with a 422 (validation.failed, with details.dsar = ["org_scope_unsupported"]), and nothing happens: erasure and access act on a person's whole account, which can belong to other organizations too, so a key your organization holds cannot trigger them. Data-subject requests are executed by the platform's privacy executor instead (see above). Every other event type is acknowledged and ignored.
Consent, cookies and the ePrivacy half
Consent is a separate obligation from the ones above, and it is the one your readers actually see. Forumulate does not ship a consent banner of its own and does not pick a vendor for you: you register the CMP you already use, and the reader surfaces honour it.
Cookies & consent in the console registers your CMP and its hosts.
Receipts are written per space and readable at the public receipts endpoint above, so you can prove what a given reader agreed to.
Nothing that waits on consent loads before it is given. A tag that may load must also be able to call home, so registering a vendor widens the page's content-security policy for that vendor and nothing else.
See the five Cookies & consent pages for the setup itself.
Third-party requests: fonts and images
A page Forumulate serves makes no request to anybody else on its own. That matters under the GDPR: loading a font or a picture from another host hands that host the reader's IP address, and a German court has fined a site for doing exactly that with Google Fonts (LG München I, 3 O 17493/20).
Fonts are self-hosted. Every font your theme or email brand names — a Google Fonts family from the picker, a stylesheet URL you paste, a font file in your custom CSS — is copied to our storage when you save, and your pages, the editor's previews and your emails load only that copy. Nothing is fetched from Google or your font host when someone reads your site. A font we have not copied yet (a few seconds after a save) shows in a metric-matched fallback face, never from the original host.
Images are proxied. A logo, cover, avatar or picture in your content that lives on another site is fetched once by our servers and served from ours. The other site never sees your readers, and that includes your feeds and your emails. "Use Gravatar" copies the picture instead of linking to gravatar.com. Uploading to your asset library is still the most direct way to host an image.
What you add yourself stays yours. Your consent manager, analytics tags, chat widget, video and iframe embeds, custom JavaScript, and image hosts you list in the theme's
imgOriginsload from where you chose — you added them, and they belong on your own sub-processor list.
Sub-processors
Ours are named in our privacy policy — hosting, DNS and the network in front of the site, object storage for uploads and backups, transactional email, payments, and crash reporting. If your own DPA requires you to list them to your customers, that section is the source.
Anything you connect yourself — an AI provider, a translation provider, a chat widget, an analytics tag — is yours, not ours. Those are bring-your-own-key by design: the credential is yours, the contract is yours, and the data goes to a provider you chose. That is deliberate, and it is also something your own sub-processor list has to say.
Where to put your own answer
Publish your policy as a LEGAL space. It gets versions, effective dates, an acceptance record per member, and a public index — which is what turns "we have a privacy policy" into something you can prove on a date. See Legal spaces and Binding and acceptance.
What's next
Cookies & consent — registering your CMP.
Legal spaces — publishing the policy itself.
Members and permissions — who can see what.
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.
Audit
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Get the audit in an organization |
| |
| File of export (audit) |
| |
| List actions in an organization |
|
Consent
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Get the consent in an organization |
| |
| Replace the consent in an organization |
| |
| Create the webhook secret in an organization |
| |
| Is a privacy-executor credential configured? |
| |
| Mint this organization’s privacy-executor credential |
| |
| Create the webhook test in an organization |
| |
| Purge the cache (consent) |
| |
| Get the consent in a space |
| |
| Replace the consent in a space |
| |
| Purge the cache (spaces) |
|
Consent Public
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Get the consent in a space (public reader) |
| |
| Create a receipt in a space |
|