Consent: any CMP, the __consent contract, and what we store
Forumulate works with any consent management platform (CMP). Cookie Munch, Cookiebot, OneTrust, Osano, Termly, Usercentrics and Didomi each have a native preset. Any other CMP works through the IAB signals (TCF v2.2, GPP) or through the vendor-neutral JavaScript contract on this page. Global Privacy Control (GPC) is honoured on top of all of them.
The contract: window.__consent (also window.__fdConsent)
The object is Cookie Munch's shared consent contract. Its neutral name is window.__consent; window.__fdConsent is the same object under its first name and stays a permanent alias, so code written against either keeps working. Forumulate reads __consent first — only when it is the contract (version: 1, a has function), since a short global like that may belong to another script — and falls back to __fdConsent. When Forumulate provides the object itself, it publishes it under both names, never over a __consent that is not the contract.
On every reader page whose space has an active CMP, the contract answers for that CMP:
Member | Returns | Meaning |
|---|---|---|
|
| Resolves once the CMP has loaded and answered. Never rejects. |
|
|
|
| unsubscribe function | Fires after every decision or withdrawal, with all categories. |
| — | Opens the CMP's preference centre. |
|
| Hands the CMP a signed-in subject where it supports one (Cookie Munch, Didomi). Returns whether it was accepted. |
| pixels | How much of the viewport bottom the banner covers, so a floating launcher can sit above it. The same value is in the CSS variable |
has() accepts the suite's six categories: necessary, preferences, statistics, marketing, support_chat and referrals. It also accepts the usual aliases (analytics, functional, advertising, …).
If a CMP has no category of its own for support_chat, the answer follows preferences. Likewise referrals follows marketing.
Identity: the same person on your product and on your docs
When a reader is signed in on your docs or community, Forumulate tells the page's consent manager who they are by calling identify(token). Their consent choices then follow them between your product's own pages and ours, and across devices.
Cookie Munch checks every identity token the same way: aud: cookiemunch:<cbid>, token_use: "template", and exp - iat of at most 120 seconds. It knows the person as (iss, sub). A token can come from one of two places, and Forumulate picks one per reader:
The IdP's own server mint (Atlas). If the reader signed in through Atlas and the Atlas provider holds the instance secret key, our server asks Atlas for the token:
POST /v1/sessions/{sid}/integration_tokens/cookiemunch?cbid=<your cbid>. Thesidis the Atlas session the reader signed in with. This is the same token your product's pages get fromgetToken({ for: 'cookiemunch' }), so it needs no Cookie Munch configuration beyond the issuer your site already trusts.Forumulate-signed, acting for the reader's IdP. This works for any IdP: Atlas without a saved key, any OIDC provider, or
external_jwtwith an issuer and a JWKS. Forumulate signs{ iss: "https://forumulate.com", sub: <the reader's subject at their IdP>, aud: "cookiemunch:<cbid>", token_use: "template", iat, exp: iat + 120 }with an Ed25519 key (alg: EdDSA, with akid). The public keys are published athttps://forumulate.com/.well-known/jwks.json. Cookie Munch accepts the token only when your site lists Forumulate in its config, withactsForset to your IdP's issuer:{ "consent": { "identifyIssuers": [ { "issuer": "https://forumulate.com", "jwksUrl": "https://forumulate.com/.well-known/jwks.json", "actsFor": "<your IdP's issuer>" } ] } }With
actsFor, Cookie Munch treats the token'ssubas the same person your IdP's own tokens name. You do not need to work out these values:GET /orgs/{orgId}/consent/identityreturns the exactidentifyIssuersentries for your organization. It also shows which mint each of your reader IdPs gets.
When Atlas is configured but declines (for example, the integration is not enabled), Forumulate signs instead, provided your site lists us.
Which IdPs Forumulate vouches for. One Forumulate issuer serves every organization on the platform. So we only sign for an issuer whose keys came from the issuer itself: its own discovery document, a JWKS on the issuer's host, or Atlas's per-instance discovery. A provider that names some issuer alongside a key set hosted elsewhere, or an external_jwt that uses a shared secret, still signs readers in, but we do not vouch for it. Otherwise any organization could make Forumulate assert identities at somebody else's IdP. Those readers get issuer_not_anchored.
How the page does it:
The reader signs in through your IdP, either with the token hand-off or the redirect button. We keep their IdP issuer, subject and session id on the server. None of it reaches the browser.
A reader page with
<html data-viewer="in">callsPOST /public/orgs/{orgId}/consent/identity-tokenwith{ "spaceId": "…" }, same-origin and with the reader's own cookie. It does this on each page load and whenever the reader signs in or out. It then passes the token towindow.__consent.identify(token).When the reader signs out, the page calls
identify(null), but only if Forumulate made the earlieridentifycall. Otherwise it would wipe an identity your own pages gave the consent manager.
The endpoint answers 200 { token, expiresAt } with Cache-Control: no-store. When nothing can be minted it answers 404, and the page calls nothing. The error.reason says why:
| Meaning |
|---|---|
| The reader did not sign in through an IdP. |
| The space's provider takes no identity token. Only |
| The reader's issuer is no longer configured. |
| We do not vouch for this issuer (see above). |
| Forumulate signing is off on this deployment. |
| The IdP declined. |
A request with no reader signed in gets 401, and a cross-site request gets 403. Neither the token nor any key is ever logged.
What to set up, once:
Atlas server mint: save the instance's secret key (
config.secretKey, scopesessions:write) on the Atlas reader sign-in provider. It is write-only: a read answerssecretKeySet: true. Then enable thecookiemunchintegration on the instance with your site id.Any other IdP, or as a fallback: paste the
identifyIssuersentries fromGET /orgs/{orgId}/consent/identityinto your Cookie Munch site config.Both: set the space or org consent provider to
cookiemunch(ormanaged) with itscbid.
A reader who used "Sign in with Atlas" from another Atlas account has no session on your instance to mint for. If your site lists Forumulate, they are identified by a Forumulate-signed token instead.
If your page or CMP already provides __consent or __fdConsent
Forumulate uses yours and installs nothing. That is how a CMP implements the contract natively, and how the custom provider works: your own platform defines window.__fdConsent, and everything below uses it.
Per-vendor adapters
When the CMP does not provide the contract, our adapter for it does, using only the vendor's public browser API:
Provider | Read from | Undecided while | Preference centre |
|---|---|---|---|
Cookie Munch |
|
|
|
Cookiebot |
|
|
|
OneTrust |
| a group is inactive and the banner is still open |
|
Osano |
| no |
|
Termly |
| no stored Termly consent |
|
Usercentrics |
| consent still required |
|
Didomi |
|
|
|
Any other ( |
| the TCF event is not | the CMP's own |
TCF purposes to suite categories. Every category needs P1 (store on a device).
preferences: P1.statistics: P1 plus P8, P9 or P10.marketing: P1 plus P3 and P4, or P1 plus P7.referrals: P1 plus P7.
GPP. The EU section is read like TCF. A US opt-out of sale, sharing or targeted advertising refuses marketing and referrals.
GPC. navigator.globalPrivacyControl === true refuses marketing and referrals, whatever the CMP says.
"Cookie settings" in the reader footer appears for every connected CMP. It calls that vendor's preference centre, or __fdConsent.request() for any other.
What we store, and when
GET /api/public/orgs/{org}/cookie-manifest lists every cookie, localStorage/sessionStorage key and cache your readers can be given, with your real space ids in the names. Each item carries its suite category, lifetime, httpOnly / host-only, when it is set, and a purpose in every locale your sites publish. Analytics vendors your spaces load, and a connected chat widget, are included. The manifest has no credentials and is cacheable, so a CMP's scanner or your cookie policy can read it directly.
While __fdConsent answers, our own storage follows it:
Anything in
preferences,statistics,marketing,referralsorsupport_chatis written only whenhas(category) === true.Such storage is deleted in the same tick that the reader withdraws its category.
The reader's language choice (
forumulate_reader_lang) still works for the visit as a session cookie, and becomes lasting oncepreferencesis allowed.Necessary storage is never gated: sign-in, the consent record itself, the cart and a draft being typed.
On a page with no CMP, storage behaves exactly as it did before.
A reader who has not decided gets none of the gated storage: no lasting language cookie, no reader preference keys, no measurement keys, no chat keys, no referral or attribution storage.
Our status, changelog and roadmap widgets on your site follow the same rule. If your page exposes __fdConsent, their preference keys wait for has('preferences').
We keep no analytics device id. Measurement is counted per tab and deduplicated on our side.
Connect a CMP in one call
PUT /api/orgs/{org}/consent takes connect:{…} beside the config. Each provider has a handler, and the answer lists every step, its status and how it was done.
Step | Cookie Munch | Cookiebot | OneTrust | Osano | Termly | Usercentrics | Didomi | custom |
|---|---|---|---|---|---|---|---|---|
site | api | manual | manual | manual | manual | manual | manual | — |
webhook (+ secret) | api | — | manual | — | — | — | — | manual |
executor (rights requests) | api | manual | manual | manual | manual | manual | manual | manual |
cookie declaration | api | manual | manual | manual | manual | manual | manual | manual |
reader banner | api | api | api | api | api | api | api | api |
Cookie Munch: connect: {orgProvisioningKey: "cookiemunch_live_org_…", cbid?}. The key needs sites:write, webhooks:read, webhooks:write and dsar:write. Missing scopes are a 422 that names them, and nothing is changed. The connect:
creates or reuses the site;
creates or rolls the webhook and stores its secret;
registers Forumulate as an executor (contract v2), with the key and the completion-webhook secret sent server to server;
declares the cookie manifest on the site;
points your readers at the banner.
Re-running it is safe.
Every other provider: connect: {executor?: true, completionWebhook?: {url, secret}, rotateWebhookSecret?: true}. We do our half and answer each vendor-side step with the instruction and the exact values (webhookUrl, executorUrl, cookieManifestUrl). An executor key or webhook secret that a human has to paste is returned once, in shownOnce.
?validate=true (or ?dryRun=true) with connect plans the steps and writes nowhere. It does ask Cookie Munch about an orgProvisioningKey (GET /v1/me, read-only): a key Cookie Munch does not know, or one missing sites:write, webhooks:write or dsar:write, is refused 422 by the preview exactly as the connect would refuse it. GET /api/orgs/{org}/consent shows:
the last connect;
what the current provider can automate (
capabilities);the executor block:
enabled,version,completionWebhook.
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 SDKs are generated from the same spec), and an agent reaches the organization-scoped ones through the MCP agent server — its api_* family tools, or call_api. Where a CLI command or a hand-made MCP tool exists too, the last column names it. Operation names link to the API reference.
Consent Identity
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Send a token request in an organization |
|
Consent Identity Setup
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Get the identity in an organization |
|