> ## Documentation Index
> Fetch the complete documentation index at: https://docs.idesify.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Consent management

> Server-driven purposes and legal clauses, recording the subject's decision

```ts theme={null}
import { createConsent } from "idesify/consent";
```

The widget fetches your tenant's active consent template, renders the purposes as toggles alongside the legal clauses, collects the subject's identifier, and records their decision. It is anonymous and scoped to your portal UUID — no API key, no bearer token, no CAPTCHA.

| | |
| - | - |
| **Size** | Roughly 17 kB, about 5 kB gzipped |
| **Dependencies** | None beyond the `idesify` package — no camera, no engine |
| **Rendering** | Pure DOM plus a scoped stylesheet, no external assets |

<Note>
  This is the **public** consent surface: the subject asserts their own identifier and the server records their decision. Managing consent state for an already-authenticated user is a different API — contact support if that is what you need.
</Note>

## Quickstart

```ts theme={null}
import { createConsent } from "idesify/consent";

const controller = createConsent({
  container: document.getElementById("consent")!,
  tenantUuid: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", // your portal UUID
  language: navigator.language,   // filters templates, sets Accept-Language and SDK copy
  jurisdiction: "CL",             // optional — filters templates by jurisdiction
  callbacks: {
    onReady:  (template) => console.log("presenting", template.name),
    onSubmit: (result)   => console.log(result.message),
    onError:  ({ error, fatal }) => console.error(error, { fatal }),
  },
});

// Mounts and fetches immediately (autoStart defaults to true).
// later: controller.destroy();
```

That is the whole integration for the common case. The built-in form handles the toggles, the identifier field, and the three actions: **Accept all**, **Save preferences**, and **Reject all**.

The server localizes purpose and clause text from `Accept-Language`. The SDK owns only the chrome copy — buttons and badges — in `en`, `es`, and `pt`.

## How decisions are recorded

When the subject submits, the SDK serializes their per-purpose choices into the shape the server stores. Two rules are enforced before anything is sent:

* **Mandatory purposes are always recorded as granted.** A purpose marked mandatory is coerced to accepted regardless of the toggle, and its toggle renders locked on.
* **Every purpose in the template is included** — the SDK iterates the template's purposes, not only the ones the user touched.

| Action | Status | Purposes recorded |
| - | - | - |
| **Accept all** | `accepted` | Every purpose granted |
| **Save preferences** | `accepted` | Current toggles, mandatory forced on |
| **Reject all** | `rejected` | Mandatory granted, everything else denied |

"Save preferences" is hidden when the template has no optional purposes.

## createConsent(options)

| Option | Type | Notes |
| - | - | - |
| `container` | `HTMLElement` | **Required.** Mount target. |
| `tenantUuid` | `string` | **Required.** Your portal UUID. |
| `baseUrl` | `string` | Endpoint prefix. Override for another origin or a proxy path. |
| `language` | `string` | BCP-47. Filters templates, sets `Accept-Language`, and selects the SDK's copy (`en`, `es`, `pt`; falls back to `en`). |
| `jurisdiction` | `string` | Optional filter forwarded to the templates query. |
| `templateId` | `string` | Pick a specific template when your tenant has several. Defaults to the first returned. |
| `userIdentifier` | `string` | Preset the subject's identifier. When set, the identifier input is omitted and this value is submitted. |
| `colorScheme` | `"light" \| "dark" \| "system"` | Default `system`. |
| `accent` | `string` | Brand accent, any CSS color. Drives the switches, primary button, and focus ring. |
| `slots` | `ConsentUISlots` | `labels` for per-string copy; `renderTerminal` to replace the success screen. |
| `callbacks` | `ConsentCallbacks` | `onReady`, `onSubmit`, `onError`. |
| `autoStart` | `boolean` | Fetch and mount immediately. Default `true`. |

### ConsentController

| Member | Description |
| - | - |
| `start()` | Fetch templates and mount. Idempotent. |
| `submit(decision)` | Record a decision programmatically. The mounted form calls this too. |
| `destroy()` | Remove the UI, drop listeners, abort in-flight requests. |
| `state()` | One of `idle`, `loading`, `ready`, `submitting`, `complete`, `error`. |
| `on(event, handler)` / `off(event, handler)` | Emitter mirror of the callbacks. `on` returns an unsubscribe function. |

## Server types

```ts theme={null}
interface ConsentTemplate {
  id: string;
  name: string;                 // shown as the widget title
  version: number;
  description?: string;         // shown as the subtitle
  jurisdiction: string;
  language: string;
  purposes: ConsentPurpose[];
  clauses: ConsentClause[];
  effective_date?: string;      // ISO-8601
  policy_id?: string;
}

interface ConsentPurpose {
  id: string;
  key: string;                  // the wire key a decision is recorded against
  label: string;                // localized server-side
  description: string;
  is_mandatory: boolean;        // locked on, and coerced to granted
  retention_period_days?: number;
}

interface ConsentClause {
  id: string;
  title: string;
  content: string;              // rendered as text, never as HTML
  is_mandatory: boolean;
  expandable?: boolean;
  compliance_frameworks?: string[];  // shown as chips
}
```

### Decision and result

```ts theme={null}
interface ConsentDecision {
  status: "accepted" | "rejected";
  purposes: Record<string, boolean>;  // purpose key to granted; mandatory forced true
  userIdentifier: string;             // email or national ID, up to 255 characters
}

interface ConsentResult {
  message: string;                    // server confirmation copy
}
```

## Headless usage

Bring your own UI, or a preset identifier from your session, and drive the submit directly. The widget still fetches the template so `submit` knows the purpose keys and can coerce the mandatory ones.

```ts theme={null}
const controller = createConsent({
  container,
  tenantUuid,
  userIdentifier: session.email,      // hides the built-in identifier input
  callbacks: { onSubmit: (r) => toast(r.message) },
});

// after your own toggles collect choices:
await controller.submit({
  status: "accepted",
  purposes: { marketing: true, analytics: false },
  userIdentifier: session.email,
});
```

`submit()` rejects if the template has not loaded yet. Wait for `state()` to reach `ready`, or for the `ready` event.

## Errors

| Class | When |
| - | - |
| `ConsentValidationError` | Field errors on `.errors`. The form re-shows itself with the field marked. Not fatal. |
| `ConsentGatewayError` | Any other non-2xx. Carries `.status` and a best-effort `.body`. |
| `ConsentNoTemplateError` | Your tenant exposes no active template, or `templateId` did not match. Fatal — there is nothing to render. |

```ts theme={null}
import { ConsentValidationError } from "idesify/consent";

callbacks: {
  onError: ({ error }) => {
    if (error instanceof ConsentValidationError) {
      console.warn("fix these fields:", error.errors);
    }
  },
}
```

## Privacy notes

* The subject **asserts** their own identifier. This is an anonymous surface, so the identifier is a claim, not a proof — never treat a recorded decision as authentication.
* The endpoint is write-only. The widget records a decision and cannot read existing ones back.
* Clause `content` is rendered with `textContent`, never as HTML, so server-supplied copy cannot inject markup into your page. Line breaks are preserved.
* Responses are enumeration-safe. A `templateId` that does not exist, or belongs to another tenant, returns the same generic error as any other failure — probing for valid IDs yields nothing.

## Embedding

No camera, no engine, no secure-context requirement. See [Embedding requirements](/sdk/embedding#rights-and-consent-widgets).
