# Manage user consent and opt-out

Grant and revoke a user's consent categories, set their opt-out preferences and read workspace consent and enforcement stats.

## Prerequisites

- Enterprise installed (`enterprise.enabled: true`). The routes are served in dev mode, so a license is not required. See [Licensing & Features](/explanation/platform/licensing/).
- A Workspace with [`spec.privacy`](/reference/core/workspace/#privacy) set. This provisions the privacy-api.
- The user's pseudonymous id (`virtualUserId`) if you call privacy-api directly. The service stores whatever key it receives, so a raw user id records state for a subject that matches no session or memory, and returns no error.
- Access to the privacy-api Service from inside the cluster, and a ServiceAccount token it accepts.

If you build an application, call the dashboard's workspace routes instead of privacy-api. They pseudonymise the user id for you and proxy to privacy-api with identical bodies. See [Workspace routes](/reference/platform/privacy-api/#workspace-routes) for the paths and how they scope the user.

The steps below call privacy-api directly. Every route, body and response field is in the [Privacy API reference](/reference/platform/privacy-api/).

## Reach privacy-api

1. Forward the Service. It is named `privacy-<workspace>` and lives in the workspace namespace.

   ```bash
   kubectl port-forward -n <workspace-namespace> svc/privacy-<workspace> 8080:8080
   ```

2. Mint a token for a ServiceAccount in the workspace namespace, with the audience `omnia-session-api`. The examples use `$SA_TOKEN` and `http://localhost:8080`.

   ```bash
   export SA_TOKEN=$(kubectl create token default -n <workspace-namespace> --audience omnia-session-api)
   ```

## Grant or revoke consent

Six consent categories exist. `memory:identity`, `memory:location` and `memory:health` need an explicit grant before memory writes in that category succeed. The other three need none. See [Consent categories](/reference/platform/privacy-api/#consent-categories).

1. Grant categories. The service applies `grants` first, then `revocations`.

   ```bash
   curl -sS -X PUT \
     http://localhost:8080/api/v1/privacy/preferences/<virtual-user-id>/consent \
     -H "Authorization: Bearer $SA_TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"grants": ["memory:identity", "memory:location"]}'
   ```

   The response is the resulting state (illustrative values):

   ```json
   {
     "grants": ["memory:identity", "memory:location"],
     "defaults": ["memory:context", "memory:history", "memory:preferences"],
     "denied": ["memory:health"]
   }
   ```

2. Revoke a category.

   ```bash
   curl -sS -X PUT \
     http://localhost:8080/api/v1/privacy/preferences/<virtual-user-id>/consent \
     -H "Authorization: Bearer $SA_TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"revocations": ["memory:location"]}'
   ```

   privacy-api pushes the revocation to the workspace's memory-api instances, which then apply the MemoryPolicy `consentRevocation.action` to memories already stored in that category. Each grant and revocation also writes a row to the privacy audit hub.

## Set or remove an opt-out

Opt-out suppresses recording at a scope (`all`, `workspace` or `agent`) instead of per category.

1. Set an opt-out. `target` names the workspace or agent, and is omitted for `all`.

   ```bash
   curl -sS -X POST http://localhost:8080/api/v1/privacy/opt-out \
     -H "Authorization: Bearer $SA_TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"virtualUserId": "<virtual-user-id>", "scope": "workspace", "target": "my-workspace"}'
   ```

   A successful call returns `204`.

2. Remove it by sending the same body with `DELETE`.

   ```bash
   curl -sS -X DELETE http://localhost:8080/api/v1/privacy/opt-out \
     -H "Authorization: Bearer $SA_TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"virtualUserId": "<virtual-user-id>", "scope": "workspace", "target": "my-workspace"}'
   ```

## Read stats

Both stats routes need the `workspace` query parameter. Enforcement stats filter on the Workspace UID, not its name.

1. Read the Workspace UID.

   ```bash
   export WS_UID=$(kubectl get workspace my-workspace -o jsonpath='{.metadata.uid}')
   ```

2. Read the stats.

   ```bash
   curl -sS "http://localhost:8080/api/v1/privacy/enforcement-stats?workspace=$WS_UID" \
     -H "Authorization: Bearer $SA_TOKEN"

   curl -sS "http://localhost:8080/api/v1/privacy/consent/stats?workspace=$WS_UID" \
     -H "Authorization: Bearer $SA_TOKEN"
   ```

   The consent stats route requires the parameter but ignores its value.

## Verify

1. Read the user's consent state. It returns the same `grants`, `defaults` and `denied` fields as the write.

   ```bash
   curl -sS \
     http://localhost:8080/api/v1/privacy/preferences/<virtual-user-id>/consent \
     -H "Authorization: Bearer $SA_TOKEN"
   ```

2. Read the user's opt-outs. A `404` means the user has no stored preferences.

   ```bash
   curl -sS http://localhost:8080/api/v1/privacy/preferences/<virtual-user-id> \
     -H "Authorization: Bearer $SA_TOKEN"
   ```

3. Confirm that opt-out is enforced in the workspace.

   ```bash
   kubectl get workspace my-workspace \
     -o jsonpath='{.status.conditions[?(@.type=="OptOutEnforced")]}'
   ```

   `True` with reason `PrivacyEnforced` means privacy-api is serving. See the [`OptOutEnforced` condition](/reference/core/workspace/#optoutenforced) for the `False` reasons.

## Troubleshooting

### A consent write returns 400

The body names a category outside the six registered ones. The response reads `unknown consent category: <value>`. Use a value from [Consent categories](/reference/platform/privacy-api/#consent-categories).

### Sessions show `[OPTED_OUT]` content or memories stop appearing

If the user has opted out, the sentinel and the missing memories are expected. If they have not, privacy-api may be unavailable: while `spec.privacy` is set and privacy-api cannot be resolved, session-api and memory-api treat every subject as opted out. Check the `OptOutEnforced` condition. `PrivacyUnavailable` means privacy-api is not serving yet. The services re-resolve it periodically and recover without a restart.

To alert on an outage, watch `omnia_session_api_content_suppressed_total{reason="privacy-unavailable"}`. The `user-opted-out` series is ordinary traffic. See [Behaviour when privacy-api cannot answer](/reference/platform/privacy-api/#behaviour-when-privacy-api-cannot-answer).

### The service logs `PRIVACY NOT ENFORCED`

The Workspace has no `spec.privacy`, so opt-out is off and all writes are recorded. Set `spec.privacy` and confirm the Workspace reports `status.privacyURL`.

## Related pages

- [Privacy API reference](/reference/platform/privacy-api/)
- [Handle data subject erasure (DSAR)](/how-to/privacy/handle-data-subject-erasure/)
- [Configure privacy policies](/how-to/privacy/configure-privacy-policies/)
- [SessionPrivacyPolicy](/reference/policies/sessionprivacypolicy/)
- [Workspace](/reference/core/workspace/#privacy)