Policy acceptance gate

Embed
Preview of the card
<a href="https://forumulate.com/guides/policy-acceptance-gate"><img src="https://forumulate.com/guides/_api/public/orgs/046a9bc0-d530-43ae-9bae-af7945008cda/spaces/36aa4df0-1e95-4cef-8ae9-795169441345/pages/eb18729d-08d1-4d3b-bb19-38c93643b27d/social-card.svg?v=3e7ae33eec8b" alt="Policy acceptance gate" width="600" height="315"></a>

Make your users accept your Terms of Service, Privacy Policy or any other policy before they sign up, and accept each new version before they sign in again, in any app, whatever signs your users in.

The policies are the documents your Legal space publishes. Your app asks Forumulate what a user still has to accept, shows it (with our widget, our hosted page or your own UI), and records the acceptance here. Forumulate keeps the evidence: which exact version each user saw, its content hash, how it was shown and when it was accepted.

It works with Auth0, Clerk, Supabase, Firebase, Auth.js, your own backend and mobile apps. Policy gate recipes has complete code for each. If you sign users in with Atlas, you may not need any of this page: Atlas reads the same policies through the policy source and gates sign-up and sign-in itself.

How it fits together

  1. Publish a policy in a Legal space. Every publish is a new, immutable version with its own content_url. Editing a published version is impossible, so the evidence of what someone accepted can never change under them.

  2. Ask. Before a sign-up or a sign-in, your server (or the widget) calls status for the user and the event. The answer lists each policy with ask (show it now) and blocking (do not let the event go ahead until it is accepted).

  3. Show. In one of six presentations.

  4. Record. The acceptance is stored with the version, the SHA-256 of the HTML that was shown, the presentation, whether the reader scrolled to the end, and the time on our database clock.

  5. Prove. You get a signed acceptance token. Your server verifies it, offline with your key or online with a call, before it lets the sign-up or sign-in finish.

Quick start

A sign-up form with the widget. The widget holds the form's submit until the box is ticked, then writes the token to a hidden forumulate_policy_token input and submits.

<script src="https://forumulate.com/policy-gate.js" async></script>

<form action="/signup" method="post">
  <input name="email" type="email" required />
  <forumulate-policy-gate org="ORG_ID" space="LEGAL_SPACE_ID" event="sign_up"></forumulate-policy-gate>
  <button>Create account</button>
</form>

On your server, verify the token and redeem it for the new account. A sign-up has no user id yet, so the widget hands back a receipt; claiming it records the acceptance against the account you just created.

import { ForumulateClient, verifyAcceptanceToken } from '@forumulate/sdk';

const forumulate = ForumulateClient.fromEnv(); // FORUMULATE_TOKEN: a PAT with policy:accept

export async function createAccount(email: string, policyToken: string): Promise<string> {
  const check = await verifyAcceptanceToken(policyToken, {
    keys: process.env.FORUMULATE_POLICY_GATE_KEYS, // "pgk_…:pgs_…"
    use: 'receipt',
    spaceId: process.env.FORUMULATE_LEGAL_SPACE,
    requireSatisfied: true,
  });
  if (!check.valid) throw new Error(`Policies not accepted: ${check.reason}`);

  const userId = crypto.randomUUID(); // your own account creation goes here
  await forumulate.policies.claim({
    orgId: process.env.FORUMULATE_ORG!,
    spaceId: process.env.FORUMULATE_LEGAL_SPACE!,
    receipt: policyToken,
    subject: userId,
    email,
  });
  return userId;
}

In React, the same form uses <PolicyGate> from @forumulate/react:

import { PolicyGate } from '@forumulate/react';

export function SignUpForm() {
  return (
    <form action="/signup" method="post">
      <input name="email" type="email" required />
      <PolicyGate org="ORG_ID" space="LEGAL_SPACE_ID" event="sign_up" presentation="checkbox" />
      <button>Create account</button>
    </form>
  );
}

The six presentations

Each policy has one presentation. The space sets it, per policy or for all of them, and a single call can override it. Every acceptance records which one was used.

Presentation

What the reader sees

Use it when

checkbox

"I agree to the Terms of Service and the Privacy Policy" with a box to tick. The titles open the text.

The default for sign-up forms. A deliberate act, recorded as checkbox, with almost no friction.

checkbox-modal

The same line, but clicking the box opens the text. The box ticks only after the reader presses Accept there.

You want every user to have had the text in front of them, without a popup on page load.

modal

A blocking dialog on load: the "unskippable popup". With requireScroll, Accept waits until the text was scrolled.

Re-acceptance at sign-in after a material change, and any policy your counsel wants read rather than linked.

inline-scroll

The text in a scroll box inside your form, with Accept beneath it.

Short policies, or a single-page flow where a dialog would get in the way.

page

Our hosted acceptance page. You redirect to it with a signed ticket and get a token back on your return URL.

Mobile apps, server-rendered apps with no JavaScript, and auth platforms that can only redirect (an Auth0 Action, a Firebase hook).

link

"By continuing you agree to…": no box. Pressing your own continue button is the acceptance.

Low-risk policies where an implicit agreement is enough. Recorded as link, so the evidence says exactly what happened.

The policy model's own display word (checkbox, scroll or link, the words the policy source uses) is the starting point when nothing else is set: scroll becomes modal with requireScroll. The acceptance record keeps both your presentation and that display word.

Configuration

Settings live at two levels, the organization and the Legal space. Each field is resolved on its own. A space that sets only presentation for its Terms keeps the org's reaccept for them.

For each behaviour field the first level that says something wins:

  1. the call (an attribute on the widget, a prop, a presentation override);

  2. the space's entry for that policy (policies.tos);

  3. the space's defaults;

  4. the org's entry for that policy;

  5. the org's defaults;

  6. what the policy itself declares (display, required, applies_to, reaccept);

  7. the built-in default.

status reports where each answer came from in sources, so you can tell why a policy behaves the way it does.

PUT /orgs/{orgId}/spaces/{spaceId}/policy-gate/config
{
  "defaults": { "presentation": "checkbox", "events": ["sign_up", "sign_in"] },
  "policies": {
    "tos": { "presentation": "modal", "requireScroll": true, "reaccept": "grace_days", "graceDays": 30 },
    "marketing": { "required": false, "decline": "allow" }
  },
  "ui": { "grouping": "sequence", "layout": "sheet", "vars": { "accent": "#4f46e5", "radius": "12px" } },
  "embed": { "allowedOrigins": ["https://app.example.com"], "tokenTtlSeconds": 900 },
  "page": { "returnUrls": ["https://app.example.com/policy/return", "exampleapp://policy"] }
}

PUT replaces the stored config; an absent key inherits. The body is strict: an unknown key is a 422 naming its path, never a 200 that quietly drops it. PUT /orgs/{orgId}/policy-gate/config sets the org level. GET on either answers { stored, org, resolved }, so you can see what is set where.

Every toggle

Behaviour keys go in defaults or in policies.<key>, at either level.

Key

Values

Default

What it does

presentation

checkbox, checkbox-modal, modal, inline-scroll, page, link

checkbox

How the policy is shown. See the six presentations.

required

boolean

true

Whether accepting is needed to continue. Only a required policy can be blocking.

events

["sign_up"], ["sign_in"] or both

["sign_up"]

Which events ask for it. Overrides the policy's own applies_to.

reaccept

never, next_sign_in, grace_days

next_sign_in

What happens to someone who accepted an older version once a new one takes effect. See Status rules.

graceDays

1 to 365

14

Under grace_days: how long the new version is asked without blocking.

requireScroll

boolean

false

Accept waits until the reader has scrolled to the end of the text.

showSummary

boolean

true

A re-accept shows the version's summary of changes above the text.

decline

allow, block

allow

Non-required policies only: whether declining lets the event go ahead.

dismissible

boolean

true

Non-required policies only: whether the dialog can be closed without an answer.

Gate-wide keys, by the object they sit in:

Object

Key

Values

Default

What it does

(top level)

enabled

boolean

true

The whole gate. Off, every status answers with nothing to accept.

ui

grouping

combined, sequence

combined

Several pending policies: one dialog for all of them, or one after another ("1 of 3").

ui

reader

modal, new_tab

modal

Where a policy title opens: our reader dialog, or its content_url in a new tab.

ui

layout

center, sheet, fullscreen

center

Where the dialog sits.

ui

mobileFullscreen

boolean

true

The dialog goes full screen on a narrow viewport, whatever layout says.

ui

inheritTheme

boolean

true

Start from the space theme's accent, font and radius before vars apply.

ui

vars

CSS values by name (see Look)

none

The widget's CSS custom properties, --fpg-<name>.

ui

locale

a language tag

en

The language the widget speaks when the page does not say.

ui

labels

{ "<locale>" or "*": { "<label key>": "text" } }

none

Replace any of the widget's words, for one language or all of them.

embed

allowedOrigins

origins, up to 50

[] (any)

The origins the widget may record acceptances from. Another origin gets 403 policy.origin_not_allowed.

embed

requireTicket

boolean

true

The browser must present a ticket your server signed to name its user. Off, a subject the page passes is recorded as unverified.

embed

tokenTtlSeconds

60 to 86400

600

How long an acceptance token the widget hands back stays valid.

page

returnUrls

absolute URL prefixes (https, or an app scheme), up to 50

[]

Where the hosted page may send the reader back. A return address that matches none is refused, so the page is never an open redirect.

reader

prompt

off, widget

off

Your Forumulate readers on a space with a legal gate: a plain refusal (as before), or the same widget your app users see.

Look and words

ui.vars takes any of accent, accent-contrast, background, surface, text, muted, border, radius, font, shadow, backdrop, focus and max-width. Each is a plain CSS value: a declaration block, url() or @import is refused, because the widget never makes a third-party request. You can also set them from your own stylesheet:

forumulate-policy-gate {
  --fpg-accent: #4f46e5;
  --fpg-radius: 12px;
}
forumulate-policy-gate::part(accept) {
  font-weight: 600;
}

The parts are dialog, content, accept, decline, checkbox, label and summary.

The widget's words are built in for English, German, French, Spanish and Portuguese. Override any of them with ui.labels. The keys are agree, agreeLink, accept, acceptAll, decline, later, close, continue, scrollHint, summaryHeading, dialogTitle, dialogTitleOne, optional, required, openInNewTab, step, pageTitle, pageIntro, error, declinedBlocked and and. {policies} is replaced with the linked titles, {title} with one title, and {n} and {total} with the step count. and is the word that joins the last two titles in {policies} ("Terms and Privacy Policy"), so a translation of agree reads correctly in its own language.

{ "ui": { "labels": { "*": { "agree": "I have read and accept {policies}" }, "de": { "accept": "Einverstanden" } } } }

The widget

<script src="https://forumulate.com/policy-gate.js" async> defines the <forumulate-policy-gate> element and the ForumulatePolicies global. On npm it is @forumulate/embed/policy-gate, and in React @forumulate/react.

Attribute

React prop

What it does

org, space

org, space

Required. The organization and the Legal space.

api

apiBase

The API base. Default: the script's origin + /api.

event

event

sign_up or sign_in.

ticket

ticket

The ticket your server signed: who the user is.

subject

subject

Only when the space turned tickets off.

presentation

presentation

One presentation for every policy on this call.

policies

policies

Only these keys (comma-separated; an array in React).

require-scroll

requireScroll

true or false, over the config.

grouping

grouping

Over ui.grouping.

reader

reader

Over ui.reader.

layout

layout

Over ui.layout.

locale

locale

The language.

theme

theme

light, dark or auto.

name

name

The hidden input the token is written to. Default forumulate_policy_token.

source-app

sourceApp

Recorded as sourceApp on each acceptance.

auto-submit

autoSubmit

Default true: inside a form, hold the submit until accepted, then resubmit.

preview

preview

Render a payload you pass (the .payload property) without calling the API, for a preview or a storybook.

Events are CustomEvents that bubble: ready (the payload), change ({ valid }), accepted ({ token, tokenType, expiresAt, satisfied, pending }), declined ({ key, blocking }) and error ({ code, message }). In React they are onReady, onChange, onAccepted, onDeclined and onError.

The element has .payload, .valid and .token, plus .accept() and .refresh(). With checkbox, link and inline-scroll, call .accept() from your own continue button (in React, through a ref).

Without an element, call it as a function:

// not type-checked: runs in the browser with the widget script loaded
const result = await ForumulatePolicies.require({
  org: 'ORG_ID',
  space: 'LEGAL_SPACE_ID',
  event: 'sign_in',
  ticket: ticketFromYourServer,
});
// result.tokenType: 'acceptance', 'receipt', or 'none' (nothing to accept).
// A declined required policy rejects with { code: 'declined', key }.

React's usePolicyGate() returns the same require, status and redirect, loads the script once, and refuses to run on the server.

Status rules

status answers for one subject and one event, on the database clock. Each policy gets a reason:

reason

Meaning

Asked

Blocking

sign_up

A sign-up: there is no history.

yes

if required

never_accepted

The user never accepted any version.

yes

if required

new_version

A newer version took effect and reaccept is next_sign_in.

yes

if required

grace_period

A newer version took effect, reaccept is grace_days, and the grace period has not ended. See dueAt.

yes

no

grace_expired

The grace period ended.

yes

if required

accepted

The current version is accepted.

no

no

reaccept_never

An older version was accepted and reaccept is never: it still counts.

no

no

not_for_event

The policy is not asked at this event (events).

no

no

not_yet_effective

The only version takes effect later.

no

no

upcoming names a version with a future effective_at. It can be accepted early, so a user who accepts it now is not asked again when it takes effect.

satisfied is true when nothing is blocking. pending lists the blocking keys and optional the asked, non-required ones.

The acceptance record

Each accepted policy is one row. The table is append-only: there is no update, and a database trigger refuses one.

Field

What it is

subject

Your id for the user, opaque to us.

subjectEmailHash

SHA-256 of the trimmed, lower-cased email, when you sent one. The email itself is never stored.

key, version

Which policy, which version.

contentSha256

SHA-256 of exactly the HTML that was shown.

presentation

One of the six.

display

checkbox, scroll or link: the policy source's word for what was shown.

scrolledToEnd

Whether the reader scrolled to the end of the text.

event

sign_up or sign_in.

verification

server (your server, with a PAT), ticket (the widget, with a ticket you signed), receipt (a claimed sign-up receipt) or unverified.

sourceApp

Your name for the app (web, ios).

locale

The language shown.

ip, userAgent

The end user's, when known: the widget's own request, or what your server passed.

batchId

One id per acceptance action, shared by the policies accepted together. The token's jti.

acceptedAt

Our database clock.

Read the log with GET …/policy-gate/acceptances (filter by subject, email, emailHash or key; up to 500 rows a page, with the next page at page.nextCursor), or download it with GET …/policy-gate/acceptances/export?format=csv (or json).

Tokens

Three tokens, all compact JWTs signed with HS256 by a key of the Legal space's keyring. A key has an id (pgk_…) and a secret (pgs_…). Rotate it with POST …/policy-gate/keys/rotate: the secret is shown once, and the previous key keeps verifying for overlapHours (24 by default, up to 168). Keep both in one environment variable while they overlap:

FORUMULATE_POLICY_GATE_KEYS="pgk_new:pgs_…,pgk_old:pgs_…"

Token

use

Signed by

What it says

Ticket

ticket

you

"This browser acts for user sub, for event evt." At most one hour. The hosted page's request is a ticket with a return URL (ret) and st.

Acceptance

acceptance

Forumulate

"sub accepted these exact versions, and ok: nothing blocking is left."

Receipt

receipt

Forumulate

An acceptance with no sub yet: a sign-up form accepted before the account existed. Redeem it once with claim.

Every token has aud: "forumulate:policy-gate" and a kid header naming its key. Check use. The same key signs tickets, which you hand to the browser, and acceptance tokens, so a verifier that skips use would take a ticket as proof of acceptance. verifyAcceptanceToken checks it.

An acceptance token's claims:

Claim

Meaning

iss

forumulate.

org

Organization id.

spc

Legal space id.

sub

Your user id. Absent on a receipt.

evt

sign_up or sign_in.

ok

Nothing blocking is left.

pend

The keys still blocking.

acc

[{ k, v, h, p, s, at }]: key, version, content hash, presentation, scrolled to end, accepted at (Unix seconds).

jti

The acceptance batch.

ctx

Receipts only: the IP, user agent, locale, app and email hash it was given in.

Verify offline

import { verifyAcceptanceToken } from '@forumulate/sdk';

export async function policiesOk(token: string, userId: string): Promise<boolean> {
  const r = await verifyAcceptanceToken(token, {
    keys: process.env.FORUMULATE_POLICY_GATE_KEYS,
    subject: userId,
    spaceId: process.env.FORUMULATE_LEGAL_SPACE,
    requireSatisfied: true,
  });
  if (!r.valid) console.warn('policy token refused:', r.reason);
  return r.valid;
}

The reason is one of malformed, unsupported_algorithm, unknown_key, bad_signature, expired, not_yet_valid, wrong_audience, wrong_issuer, wrong_use, wrong_subject, wrong_space, wrong_org, not_satisfied or no_keys. Only HS256 is accepted, whatever the token's header says.

The token is a plain JWT, so any JWT library verifies it, in any language, with the secret's UTF-8 bytes as the HMAC key. Check aud, use, iss, exp, sub and spc yourself.

keys also takes secret (one bare secret), jwks (a JSON Web Key Set of oct keys) or jwksUrl (the same set from a private endpoint of yours, such as one in front of a secrets manager). The keys are symmetric, so Forumulate never publishes them.

Verify online

No key on your server: ask us.

import { ForumulateClient } from '@forumulate/sdk';

const forumulate = ForumulateClient.fromEnv();

export async function verifyOnline(token: string, userId: string) {
  return forumulate.policies.verify({
    orgId: process.env.FORUMULATE_ORG!,
    spaceId: process.env.FORUMULATE_LEGAL_SPACE!,
    token,
    subject: userId,
  });
}

The hosted page

For apps that can only redirect: a mobile app, a page with no JavaScript, an auth platform's post-login hook.

  1. Add your return address to the space's page.returnUrls. A prefix is enough (https://app.example.com/policy/), and app schemes such as exampleapp:// work.

  2. Your server signs a ticket with returnUrl and a state you will check.

  3. Redirect the user to https://forumulate.com/policy/{orgId}/{spaceId}?ticket=….

  4. The page shows every pending policy and records what the user accepts.

  5. The user comes back:

    • accepted: {returnUrl}?policy_token=…&policy_token_type=acceptance&state=…;

    • nothing to accept: the same, with a token whose acc is empty and ok is true;

    • a required policy declined: {returnUrl}?policy_error=declined&state=….

  6. Your server checks state and verifies the token.

import { policyGatePageUrl, readPolicyReturn, signPolicyTicket, verifyAcceptanceToken } from '@forumulate/sdk';

const orgId = process.env.FORUMULATE_ORG!;
const spaceId = process.env.FORUMULATE_LEGAL_SPACE!;
const keys = process.env.FORUMULATE_POLICY_GATE_KEYS!;

export async function startPolicyPage(userId: string, state: string): Promise<string> {
  const ticket = await signPolicyTicket(
    { orgId, spaceId, subject: userId, event: 'sign_in', returnUrl: 'https://app.example.com/policy/return', state },
    keys,
  );
  return policyGatePageUrl({ orgId, spaceId, ticket });
}

export async function finishPolicyPage(url: string, userId: string, expectedState: string): Promise<boolean> {
  const back = readPolicyReturn(url);
  if (back.state !== expectedState || !back.token) return false;
  const r = await verifyAcceptanceToken(back.token, { keys, subject: userId, requireSatisfied: true });
  return r.valid;
}

If your server holds a PAT but not the gate key, client.policies.ticket(…) mints the ticket for you and answers with the page URL.

Recording from your own UI

Show the policies yourself, then record them from your server. Ask for the content with includeContent, show contentHtml, and send back the contentSha256 you were given: it is the proof of what was shown. A hash that does not match the live version is 409 policy.content_mismatch.

import { ForumulateClient, acceptanceItemsFrom } from '@forumulate/sdk';

const forumulate = ForumulateClient.fromEnv();
const space = { orgId: process.env.FORUMULATE_ORG!, spaceId: process.env.FORUMULATE_LEGAL_SPACE! };

export async function pendingPolicies(userId: string) {
  return forumulate.policies.status({ ...space, subject: userId, event: 'sign_in', includeContent: true });
}

export async function recordAccepted(userId: string, ip: string, userAgent: string) {
  const status = await forumulate.policies.status({ ...space, subject: userId, event: 'sign_in' });
  const { token, satisfied } = await forumulate.policies.accept({
    ...space,
    subject: userId,
    event: 'sign_in',
    ip,
    userAgent,
    policies: acceptanceItemsFrom(status, { presentation: 'modal', scrolledToEnd: true }),
  });
  return { token, satisfied };
}

Record only what the user was actually shown and accepted. acceptanceItemsFrom takes every policy the status asks for; pass keys to narrow it.

Accessibility

  • The widget uses native form controls: the checkbox is a real checkbox with its label, and every action is a button, so it works with a keyboard and a screen reader.

  • requireScroll disables Accept until the end of the text is reached, and scrollHint says why. The scroll box is focusable, so a keyboard user can scroll it.

  • prefers-reduced-motion turns the dialog's animation off.

  • ui.mobileFullscreen keeps the text readable on a phone.

  • --fpg-focus sets the focus ring. Keep it visible.

  • All of the words can be translated or replaced with ui.labels.

If you build your own UI, keep the same properties: a real label for the checkbox, the text reachable by keyboard, and a visible focus.

Data and data-subject requests

  • What we store: your opaque subject id, an email hash (never the email), the version and hash of what was shown, how it was shown, the time, and the IP and user agent when known. The policy text is in the immutable version, not copied per user.

  • Who controls it: you. Forumulate records acceptances for your app as your processor. The records belong to your organization, and tenant isolation is enforced in the database.

  • Access: filter the log by subject, email or emailHash, or export it as CSV or JSON.

  • Erasure: POST /orgs/{orgId}/policy-gate/subjects/erase with subject, email or emailHash deletes that person's rows. It is the one exception to append-only. A privacy executor erases and exports them through /privacy/executor/{orgId}/policy-acceptances/erase and …/export, as part of a data-subject request that spans your stack.

  • Keep evidence before you erase: an acceptance is often your proof of consent. Export it first if your retention policy says to.

Errors

Code

Status

Meaning

policy.content_mismatch

409

The contentSha256 sent is not the hash of the version's HTML.

policy.version_not_live

409

That version is not the current one or an upcoming one. Ask status again.

policy.ticket_invalid

401

The ticket did not verify. details.reason says why.

policy.origin_not_allowed

403

The page's origin is not in embed.allowedOrigins.

policy.scroll_required

422

requireScroll is on and the acceptance did not scroll to the end.

policy.receipt_claimed

409

The receipt was already redeemed.

Access and scopes

Capability

Grants

policy.read

status, the log, the export, online verify

policy.accept

record, claim, mint tickets

A personal access token for your app's server needs the policy:read scope to check and policy:accept to record. Keep it, and the gate key, on the server.

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.

Policy Gate Admin

Operation

Request

What it does

Shortcuts

PolicyGateAdmin_getOrg

GET /orgs/{orgId}/policy-gate/config

Get the organization’s policy gate defaults

client.policyGateAdmin.getOrg()

PolicyGateAdmin_putOrg

PUT /orgs/{orgId}/policy-gate/config

Replace the organization’s policy gate defaults

client.policyGateAdmin.putOrg()

PolicyGateAdmin_patchOrg

PATCH /orgs/{orgId}/policy-gate/config

Change part of the organization’s policy gate defaults

client.policyGateAdmin.patchOrg()

PolicyGateAdmin_getSpace

GET /orgs/{orgId}/spaces/{spaceId}/policy-gate/config

Get a Legal space’s policy gate config

client.policyGateAdmin.getSpace()

PolicyGateAdmin_putSpace

PUT /orgs/{orgId}/spaces/{spaceId}/policy-gate/config

Replace a Legal space’s policy gate config

client.policyGateAdmin.putSpace()

PolicyGateAdmin_patchSpace

PATCH /orgs/{orgId}/spaces/{spaceId}/policy-gate/config

Change part of a Legal space’s policy gate config

client.policyGateAdmin.patchSpace()

PolicyGateAdmin_listKeys

GET /orgs/{orgId}/spaces/{spaceId}/policy-gate/keys

List the policy gate signing keys

client.policyGateAdmin.listKeys()

PolicyGateAdmin_rotate

POST /orgs/{orgId}/spaces/{spaceId}/policy-gate/keys/rotate

Rotate the policy gate signing key

client.policyGateAdmin.rotate()

PolicyGateAdmin_revoke

DELETE /orgs/{orgId}/spaces/{spaceId}/policy-gate/keys/{kid}

Revoke a policy gate signing key

client.policyGateAdmin.revoke()

PolicyGateAdmin_erase

POST /orgs/{orgId}/policy-gate/subjects/erase

Erase a subject’s policy acceptances (DSAR)

client.policyGateAdmin.erase()

The Forumulate teamUpdated