Manage User Consent and Opt-Out
This guide shows how to manage a single user’s privacy preferences against the per-workspace privacy-api: granting and revoking granular consent categories, setting scope-based opt-out preferences, and reading aggregate consent and enforcement stats.
privacy-api owns per-user consent grants and opt-out preferences (each workspace has its own consent database). Memory and PII-redaction enforcement in memory-api and session-api reads this state to decide what may be stored — so these are the knobs that drive per-user enforcement.
Prerequisites
Section titled “Prerequisites”- A Workspace with
spec.privacyset. This is the trigger that provisions the privacy-api; without it there is no consent service to call. - The user’s pseudonymized identifier (
virtual_user_id— the samePseudonymizeIDvalue the facade writes for sessions and memory, never a raw email or user id). Consent, opt-out, and session data are all keyed by this pseudonym, so a raw id matches nothing. - Network access to privacy-api. Its JSON API is authenticated with a Kubernetes ServiceAccount token and is not exposed externally by default, so call it from inside the cluster with a valid token.
# Forward the workspace's privacy-api locally (service name is privacy-<workspace>).kubectl port-forward -n <workspace-namespace> svc/privacy-<workspace> 8080:8080All examples below assume $SA_TOKEN holds a valid ServiceAccount bearer token
and the service is reachable at http://localhost:8080.
Consent categories
Section titled “Consent categories”Consent is granted per category. The platform defines a fixed set of categories; two tiers exist:
| Category | Requires explicit grant? |
|---|---|
memory:preferences |
No — granted by default |
memory:context |
No — granted by default |
memory:history |
No — granted by default |
memory:identity |
Yes |
memory:location |
Yes |
memory:health |
Yes |
analytics:aggregate |
Yes |
Categories that do not require an explicit grant are treated as consented unless
the user opts out; the sensitive categories (memory:identity, memory:location,
memory:health, analytics:aggregate) are denied until explicitly granted.
Grant or revoke consent
Section titled “Grant or revoke consent”PUT /api/v1/privacy/preferences/{userID}/consent applies grants and/or
revocations in one call. The body has two lists — grants and revocations:
curl -sS -X PUT \ http://localhost:8080/api/v1/privacy/preferences/<pseudonymized-user-id>/consent \ -H "Authorization: Bearer $SA_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "grants": ["memory:identity", "memory:location"], "revocations": ["analytics:aggregate"] }'The response is the user’s resulting consent state:
{ "grants": ["memory:identity", "memory:location"], "defaults": ["memory:preferences", "memory:context", "memory:history"], "denied": ["memory:health", "analytics:aggregate"]}grants— categories the user has explicitly granted.defaults— categories consented by default (no explicit grant needed).denied— sensitive categories not currently granted.
Revoking a category that was never granted is a silent no-op. Revocations are
also fanned out to downstream memory-api instances so already-stored memories in a
revoked category can be enforced. Each grant and revocation is written to the
privacy audit log (consent_granted / consent_revoked).
Read a user’s consent state
Section titled “Read a user’s consent state”GET /api/v1/privacy/preferences/{userID}/consent returns the same
grants / defaults / denied shape without mutating anything:
curl -sS \ http://localhost:8080/api/v1/privacy/preferences/<pseudonymized-user-id>/consent \ -H "Authorization: Bearer $SA_TOKEN"Set an opt-out preference
Section titled “Set an opt-out preference”Opt-out is separate from consent: it suppresses recording/enforcement at a
scope rather than per category. POST /api/v1/privacy/opt-out sets one:
curl -sS -X POST http://localhost:8080/api/v1/privacy/opt-out \ -H "Authorization: Bearer $SA_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "userId": "<pseudonymized-user-id>", "scope": "workspace", "target": "my-workspace" }'Fields:
| Field | Required | Values / notes |
|---|---|---|
userId |
yes | The pseudonymized subject id. |
scope |
yes | all, workspace, or agent. |
target |
for workspace / agent |
The workspace or agent name. Not needed for all. |
A successful set returns 204 No Content.
Remove an opt-out with the same body and DELETE /api/v1/privacy/opt-out:
curl -sS -X DELETE http://localhost:8080/api/v1/privacy/opt-out \ -H "Authorization: Bearer $SA_TOKEN" \ -H "Content-Type: application/json" \ -d '{"userId": "<pseudonymized-user-id>", "scope": "workspace", "target": "my-workspace"}'Fetch a user’s current opt-out preferences with
GET /api/v1/privacy/preferences/{userID}:
curl -sS http://localhost:8080/api/v1/privacy/preferences/<pseudonymized-user-id> \ -H "Authorization: Bearer $SA_TOKEN"A 404 Not Found means the user has no stored preferences yet.
Read enforcement and consent stats
Section titled “Read enforcement and consent stats”Both stats endpoints are workspace-scoped and require a workspace query
parameter.
Enforcement stats — how much privacy enforcement has actually happened (derived from the central audit hub):
curl -sS "http://localhost:8080/api/v1/privacy/enforcement-stats?workspace=my-workspace" \ -H "Authorization: Bearer $SA_TOKEN"{ "piiBlocked": 42, "redactions": 128}piiBlocked counts opt-out write blocks; redactions counts PII redaction
events. This is the same read path the dashboard’s enforcement-stats view uses.
Aggregate consent stats across users in the workspace:
curl -sS "http://localhost:8080/api/v1/privacy/consent/stats?workspace=my-workspace" \ -H "Authorization: Bearer $SA_TOKEN"What happens when privacy-api is unavailable
Section titled “What happens when privacy-api is unavailable”Opt-out enforcement depends on privacy-api being reachable. The behaviour when it is not depends on whether it was configured in the first place, and the two cases are deliberately opposite:
| privacy-api | Behaviour | Rationale |
|---|---|---|
| Configured but unavailable | The subject is treated as opted out — writes are dropped | The service that knows the subject’s choice cannot be asked, so the conservative answer is assumed |
| Not configured at all | The subject is treated as opted in — writes proceed | spec.privacy is optional; an unconfigured component must not halt the product |
The first case is why a privacy-api outage looks like a sudden stop in recorded sessions and memories rather than an error: the writes are being refused on purpose. It is logged at info level on the affected service:
session write dropped: privacy-api unavailable, treating subject as opted outand counted separately from real opt-outs, so it can be alerted on:
# A privacy-api problem — page on this.omnia_session_api_writes_dropped_total{reason="privacy-unavailable"} > 0
# Subjects exercising their rights — expected traffic, never alert.omnia_session_api_writes_dropped_total{reason="user-opted-out"}Writes dropped because privacy-api was unavailable are not written to the
privacy audit hub and do not appear in enforcement-stats. Only a subject’s
actual decision is recorded there, so the audit trail stays a record of consent
decisions rather than of outages.
The second case is announced once at startup, because it means opt-out is not being enforced at all:
PRIVACY NOT ENFORCED: no privacy-api resolved for this workspace;user opt-out is disabled and all session writes will be recordedIf you see that line on a deployment that is meant to honour opt-outs, set
Workspace.spec.privacy and confirm the service group reports
status.privacyURL.
Checking enforcement without reading logs
Section titled “Checking enforcement without reading logs”Both postures are also reported on the Workspace itself, so you do not have to find the right pod to answer “is opt-out enforced here?”:
kubectl get workspace my-ws \ -o jsonpath='{.status.conditions[?(@.type=="OptOutEnforced")]}'True with reason PrivacyEnforced means privacy-api is serving. The False
reasons say which of the two postures you are in — PrivacyNotConfigured (no
spec.privacy; recording proceeds) or PrivacyUnavailable (privacy-api wanted
but not serving; writes are being dropped). See the
Workspace CRD reference for the
full table.
A configured workspace whose privacy-api has not come up yet
Section titled “A configured workspace whose privacy-api has not come up yet”While spec.privacy is set but privacy-api is not resolvable, session-api and
memory-api treat every subject as opted out and drop user-scoped writes. This is
the “configured but unavailable” row above, and it applies during the first few
seconds of a brand-new workspace as well as during an outage.
The services re-resolve periodically, so they start enforcing on their own once
the operator publishes status.privacyURL — no restart is needed.
Related
Section titled “Related”- Handle Data Subject Erasure (DSAR) — erase all of a user’s data (right-to-erasure).
- Configure Privacy Policies — SessionPrivacyPolicy recording, PII redaction, and encryption rules.
- SessionPrivacyPolicy CRD — full policy field reference.
- Workspace CRD — enabling
spec.privacyto provision the privacy-api.